среда, 12 октября 2016 г.

Asynchronous handlers in nginx-haskell-module

My journey into learning of how to make haskell and nginx cooperate keeps going. Below is the latest chapter Asynchronous tasks with side effects from the README in the nginx-haskell-module. All variable handlers we met so far were pure haskell functions without side effects. Inability to put side effects into pure functions has a great significance in the sense that it gives strong guarantees about the time the functions run. In haskell, functions that may produce side effects are normally wrapped inside IO monad. They can do various non-deterministic IO computations like reading or writing files, connecting to network servers etc., which, in principle, may last unpredictably long or even eternally. Despite this, having IO functions as nginx variable handlers are extremely tempting as it makes possible to perform arbitrary IO tasks during an HTTP request. To eliminate their non-probabilistic duration downside, they could be run asynchronously in green threads provided by the haskell RTS library, and somehow signal the nginx worker’s main thread after their computations finish. This is exactly what happens in special handler NGX_EXPORT_ASYNC_IOY_Y. Consider the following example.
user                    nobody;
worker_processes        2;

events {
    worker_connections  1024;
}

http {
    default_type        application/octet-stream;
    sendfile            on;

    haskell compile threaded standalone /tmp/ngx_haskell.hs '

import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy.Char8 as C8L
import           Network.HTTP.Client
import           Control.Concurrent
import           Control.Exception
import           Safe

catchHttpException = (`catch` \e ->
        return $ C8L.pack $ "HTTP EXCEPTION: " ++ show (e :: HttpException))

getResponse (C8.unpack -> url) = fmap responseBody . (parseRequest url >>=)

getUrl url = do
    man <- newManager defaultManagerSettings
    catchHttpException $ getResponse url $ flip httpLbs man
NGX_EXPORT_ASYNC_IOY_Y (getUrl)

threadDelaySec = threadDelay . (* 10^6)

delay (readDef 0 . C8.unpack -> v) =
    threadDelaySec v >> return (C8L.pack $ show v)
NGX_EXPORT_ASYNC_IOY_Y (delay)

    ';

    server {
        listen       8010;
        server_name  main;
        error_log    /tmp/nginx-test-haskell-error.log;
        access_log   /tmp/nginx-test-haskell-access.log;

        location / {
            haskell_run_async getUrl $hs_async_ya
                    "http://ya.ru";
            haskell_run_async getUrl $hs_async_httpbin
                    "http://httpbin.org";
            haskell_run_async getUrl $hs_async_hackage
                    "http://hackage.haskell.org";
            echo "------> YA.RU:\n\n$hs_async_ya\n";
            echo "------> HTTPBIN.ORG:\n\n$hs_async_httpbin\n";
            echo "------> HACKAGE.HASKELL.ORG:\n\n$hs_async_hackage";
        }

        location /rewrite {
            rewrite ^ / last;
        }

        location /delay {
            haskell_run_async delay $hs_async_elapsed $arg_a;
            echo "Elapsed $hs_async_elapsed seconds";
        }
    }
}
Notice that the haskell code was compiled with flag threaded which is important for running asynchronous tasks. Function getUrl is an HTTP client that returns the response body or a special message if an HTTP exception has happened. Inside location / there are 3 directives haskell_run_async which spawn 3 asynchronous tasks run by getUrl, and bind future results to 3 different variables accessed later by directives echo in the nginx content phase. Async variable handlers are very special. In fact, the IO task gets spawned even if the bound variable is not accessed anywhere. All the tasks are spawned during early nginx rewrite phase (before all rewrite directives) or late rewrite phase (when all location rewrites are done: this ensures that all tasks in the final rewritten location will run). The request won’t proceed to later phases until all async tasks are done. Technically, an async task signals the main nginx thread when it finishes by writing a byte into the write-end file descriptor of a dedicated self-pipe. The read-end file descriptor of the pipe are polled by the nginx event poller (normally epoll in Linux). When a task is finished, the poller calls a special callback that checks if there are more async tasks for this request and spawns the next one or finally finishes the rewrite phase handler by returning NGX_DECLINED. All types of exceptions are caught inside async handlers. If an exception has happened, the async handler writes its message in the bound variable’s data, whereas the variable handler logs it when accessed. However, for better control, you may want to catch exceptions inside your code like in the getUrl. Let’s do some tests.
curl 'http://localhost:8010/'
Here you will see too long output with the 3 http sites content, I don’t show it here. Let’s run 20 requests simultaneously.
for i in {1..20} ; do curl -s 'http://localhost:8010/' & done
20 times longer output! Let’s make a timer for 20 seconds from 20 parallel requests.
for i in {1..20} ; do curl -s "http://localhost:8010/delay?a=$i" & done
Elapsed 1 seconds
Elapsed 2 seconds
Elapsed 3 seconds
Elapsed 4 seconds
Elapsed 5 seconds
Elapsed 6 seconds
Elapsed 7 seconds
Elapsed 8 seconds
Elapsed 9 seconds
Elapsed 10 seconds
Elapsed 11 seconds
Elapsed 12 seconds
Elapsed 13 seconds
Elapsed 14 seconds
Elapsed 15 seconds
Elapsed 16 seconds
Elapsed 17 seconds
Elapsed 18 seconds
Elapsed 19 seconds
Elapsed 20 seconds
Make sure it prints out every one second: this marks that requests are processed asynchronously! In the second test we ran 20 HTTP requests simultaneously, but could run hundreds and thousands! Some servers may reject so many requests at once (despite the fact that the manager from the Network.HTTP.Client is so advanced that it can share a single connection to the same host between all requests provided it was defined at the top level like
httpManager = unsafePerformIO $ newManager defaultManagerSettings
{-# NOINLINE httpManager #-}

getUrl url = catchHttpException $ getResponse url $ flip httpLbs httpManager
). Fortunately, we can limit number of simultaneous requests with semaphores. Let’s make a semaphore that allows only 1 task at once.
sem1 = unsafePerformIO $ S.new 1
{-# NOINLINE sem1 #-}
Functions unsafePerformIO and new must be imported from modules System.IO.Unsafe and Control.Concurrent.MSem (qualified as S) respectively. This code looks ugly, nevertheless it is safe and will work as expected in our new async handlers getUrl1 and delay1.
getUrl1 url = do
    man <- newManager defaultManagerSettings
    catchHttpException $ getResponse url $ S.with sem1 . flip httpLbs man
NGX_EXPORT_ASYNC_IOY_Y (getUrl1)

delay1 (readDef 0 . C8.unpack -> v) =
    S.with sem1 (threadDelaySec v) >> return (C8L.pack $ show v)
NGX_EXPORT_ASYNC_IOY_Y (delay1)
Put the new handlers in locations / and /delay and make the 20-requests tests again to see how they change the async behavior. For example, responses from location /delay must become so long as if they were not run asynchronously, however they must be finishing not in order. Be aware that sem1 is shared between all async handlers that use it, this means that simultaneous requests to locations / and /delay will probably wait for each other: use different semaphores for different handlers when it is not desirable. Starting an async task that normally returns identical result on every new request may be unnecessarily expensive. In the above example function getUrl must presumably return the same value during a long period of time (days, months or even years). For this case there is another handler NGX_EXPORT_SERVICE_IOY_Y that runs an async task as a service. Let’s put the following service function inside our haskell code.
getUrlService url firstRun = do
    unless firstRun $ threadDelaySec 20
    getUrl url
NGX_EXPORT_SERVICE_IOY_Y (getUrlService)
(For function unless module Control.Monad must be additionally imported.) Function getUrlService accepts two arguments, the second is a boolean value that denotes whether the service runs for the first time: it is supposed to be used to skip threadDelay on the first run. Using threadDelay in a service task is very important, because without any delay nginx will restart it very often. Let’s start getUrlService.
    haskell_run_service getUrlService $hs_service_ya "http://ya.ru";
    haskell_run_service getUrlService $hs_service_httpbin "http://httpbin.org";
Directives haskell_run_service must locate in the http clause of the nginx configuration after directive haskell compile. In contrast with other types of handlers, service handlers cannot refer to variables in their arguments as soon as nginx variable handlers always refer to a request which is not possible here. Put locations for showing data collected by the services and we are done.
        location /ya {
            echo $hs_service_ya;
        }

        location /httpbin {
            echo $hs_service_httpbin;
        }
Complex scenarios may require synchronous access to handlers with side effects. For example it could be an ad-hoc error_page redirection loop: asynchronous handlers do not suit here very well. For such cases another handler NGX_EXPORT_IOY_Y may appear useful. Below is a toy example of a synchronous handler declaration.
getIOValue = const $ return $ C8L.pack "HELLO WORLD!"
NGX_EXPORT_IOY_Y (getIOValue)
You can find all the examples shown here in file test/tsung/nginx-async.conf.

среда, 31 августа 2016 г.

Бенчмаркинг cgrep, ack и hlgrep

Когда-то я сравнивал cgrep и ack с точки зрения функциональных возможностей, пришла пора сравнить их скорости выполнения. Кроме того, недавно мне удалось серьезно оптимизировать программу hl, да так, что при рекурсивном грепе она стала во многих случаях быстрее, чем ack. Сильной оптимизации было подвергнуто время старта программы, поэтому преимущество в скорости особенно заметно при небольшом объеме входных данных. Как и ack, hl написана на perl, но реализует другую функциональность, а именно богатую подсветку входных данных, в том числе на основе сниппетов. Таким образом, рекурсивный греп в hl — возможность чисто факультативная, без большого количества опций, доступных в cgrep и ack. Тем интересней добавить его для сравнения к двум последним. Я не стал добавлять для сравнения стандартный grep. Ему по плечу такие простые задачи, как рекурсивный поиск символов, однако он настолько быстр, что с легкостью опережает на порядок все три узкоспециализированные программы (в частности, на моей машине скорость поиска символов ARGS программой grep внутри директории /usr/include/boost в четыре раза превосходит скорость cgrep). Для бенчмаркинга я использовал замечательную программу bench, которая по сути является простым объединением библиотек criterion (я писал о criterion здесь) и turtle. Поскольку я хочу погонять шелл-функциию cgr, представленную в статье, ссылка на которую приводится в самом начале данной статьи, мне придется обернуть команду для bench в bash -ci — ведь turtle в лице bench не является полноценной bash-совместимой оболочкой и не способна загружать функции bash. Для hl я тоже буду использовать шелл-функцию hlgrep, которая определена так:
function hlgrep
{
    `env which hl` -t -r -n "$@" |
            sed 's/:\(\x1b\[22;38;5;224;49m\)\([[:digit:]]\+\)'`
               `'\(\x1b\[22;38;5;248;49m\)\(\x1b\[0m\): / ⎜ \1\2\1\3 ⎜ /' |
            column -t -s-o|
            sed 's/\(\s\+\)\(\(\x1b\[22;38;5;224;49m\)[[:digit:]]\+\3'`
               `'\x1b\[22;38;5;248;49m\)\(\s\+\)/\4\2\1/' |
            hl -u -216 '\x{239C}'
}
Но даже чистую cgrep нужно будет обернуть в bash. Проблема в том, что bench подменяет канал stdin в дочерних процессах, запущенных для тестирования, на новый канал, а cgrep, определив подмену, считает, что последний аргумент его командной строки, который обычно указывает на директорию для чтения входных файлов, должен быть просто еще одним элементом для поиска. Таким образом, нам придется сначала выполнить cd, а уже затем запускать cgrep, но уже без ссылки на директорию. Проще всего это сделать в отдельной оболочке. По той же самой причине подмены stdin, в ack нужно будет добавить специальную опцию --nofilter, которая, впрочем, не должна повлиять на его скорость. Есть еще один тонкий момент, связанный с bench: он аварийно завершается в случае, если тестируемая программа возвращает не нулевое значение. Именно это делает ack, если не находит ни одного совпадения. Поэтому перед запуском bench следует проверить, что совпадения имеются для всех тестов с ack (и, автоматически, для других тестируемых программ), при этом желательно, чтобы совпадений было больше — в конце концов мы тестируем полезную нагрузку. В качестве сценариев для тестирования я выбрал поиск символов ARGS в директории /usr/include/boost на моей машине и поиск символов parse внутри относительно небольшого проекта cexmc. Во втором случае я проверил также чистые cgrep, hl и ack. Каждый тест длился по 60 секунд. И вот что из этого вышло.
bench -v2 -L60 \
'bash -ci "(cd /usr/include/boost; cgr ARGS)"' \
'bash -ci "(cd /usr/include/boost; hlgrep ARGS)"' \
'bash -ci "(cd /usr/include/boost; ack --nofilter ARGS)"' \
'bash -ci "(cd ~/devel/cexmc; cgr parse)"' \
'bash -ci "(cd ~/devel/cexmc; hlgrep parse)"' \
'bash -ci "(cd ~/devel/cexmc; ack --nofilter parse)"' \
'bash -ci "(cd ~/devel/cexmc; cgrep -r parse)"' \
'bash -ci "(cd ~/devel/cexmc; hl -r parse)"'  \
'bash -ci "(cd ~/devel/cexmc; ack --nofilter --noenv parse)"' \
-o ~/bench-cgrep-hlgrep-ack-60s.html
benchmarking bench/bash -ci "(cd /usr/include/boost; cgr ARGS)"
analysing with 1000 resamples
measurement overhead 624.0 ns
bootstrapping with 11 of 12 samples (91%)
time                 694.1 ms   (644.7 ms .. 738.5 ms)
                     0.992 R²   (0.982 R² .. 0.999 R²)
mean                 667.6 ms   (653.5 ms .. 687.1 ms)
std dev              29.20 ms   (19.88 ms .. 38.25 ms)
variance introduced by outliers: 8% (slightly inflated)

benchmarking bench/bash -ci "(cd /usr/include/boost; hlgrep ARGS)"
analysing with 1000 resamples
bootstrapping with 5 of 6 samples (83%)
time                 2.129 s    (2.004 s .. 2.287 s)
                     0.998 R²   (0.992 R² .. 1.000 R²)
mean                 2.197 s    (2.141 s .. 2.249 s)
std dev              68.12 ms   (47.76 ms .. 82.33 ms)
variance introduced by outliers: 14% (moderately inflated)

benchmarking bench/bash -ci "(cd /usr/include/boost; ack --nofilter ARGS)"
analysing with 1000 resamples
bootstrapping with 5 of 6 samples (83%)
time                 2.250 s    (2.211 s .. 2.360 s)
                     0.999 R²   (0.995 R² .. 1.000 R²)
mean                 2.239 s    (2.219 s .. 2.279 s)
std dev              32.61 ms   (5.023 ms .. 43.91 ms)
found 1 outliers among 5 samples (20.0%)
  1 (20.0%) high mild
variance introduced by outliers: 14% (moderately inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; cgr parse)"
analysing with 1000 resamples
bootstrapping with 36 of 37 samples (97%)
time                 77.04 ms   (76.21 ms .. 78.01 ms)
                     0.999 R²   (0.999 R² .. 1.000 R²)
mean                 76.23 ms   (75.70 ms .. 76.84 ms)
std dev              1.794 ms   (1.429 ms .. 2.352 ms)
found 2 outliers among 36 samples (5.6%)
  1 (2.8%) low mild
  1 (2.8%) high mild
variance introduced by outliers: 8% (slightly inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; hlgrep parse)"
analysing with 1000 resamples
bootstrapping with 33 of 34 samples (97%)
time                 94.47 ms   (93.40 ms .. 95.40 ms)
                     1.000 R²   (0.999 R² .. 1.000 R²)
mean                 93.74 ms   (93.27 ms .. 94.17 ms)
std dev              1.325 ms   (1.054 ms .. 1.740 ms)
variance introduced by outliers: 3% (slightly inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; ack --nofilter parse)"
analysing with 1000 resamples
bootstrapping with 29 of 30 samples (96%)
time                 121.5 ms   (121.0 ms .. 122.0 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 121.6 ms   (121.1 ms .. 122.2 ms)
std dev              1.449 ms   (1.174 ms .. 1.830 ms)
variance introduced by outliers: 3% (slightly inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; cgrep -r parse)"
analysing with 1000 resamples
bootstrapping with 38 of 39 samples (97%)
time                 68.35 ms   (66.80 ms .. 70.55 ms)
                     0.996 R²   (0.994 R² .. 0.999 R²)
mean                 68.03 ms   (67.52 ms .. 68.83 ms)
std dev              2.001 ms   (1.543 ms .. 2.696 ms)
found 5 outliers among 38 samples (13.2%)
  4 (10.5%) high mild
  1 (2.6%) high severe
variance introduced by outliers: 12% (moderately inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; hl -r parse)"
analysing with 1000 resamples
bootstrapping with 34 of 35 samples (97%)
time                 87.98 ms   (87.55 ms .. 88.39 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 88.04 ms   (87.77 ms .. 88.32 ms)
std dev              836.7 μs   (676.7 μs .. 1.051 ms)
variance introduced by outliers: 3% (slightly inflated)

benchmarking bench/bash -ci "(cd ~/devel/cexmc; ack --nofilter --noenv parse)"
analysing with 1000 resamples
bootstrapping with 29 of 30 samples (96%)
time                 120.8 ms   (119.5 ms .. 122.5 ms)
                     0.999 R²   (0.999 R² .. 1.000 R²)
mean                 119.7 ms   (119.3 ms .. 120.3 ms)
std dev              1.456 ms   (1.006 ms .. 2.055 ms)
found 3 outliers among 29 samples (10.3%)
  3 (10.3%) high mild
variance introduced by outliers: 3% (slightly inflated)
Графический отчет здесь. Выводы следующие: cgrep быстрее всех, особенно при большом объеме входных данных (в директории /usr/include/boost) — 0.668 против 2.20 и 2.24 сек у hlgrep и ack. При небольших объемах данных (в директории cexmc) cgrep по-прежнему лидирует, хоть и с небольшим отрывом (0.076 сек), hlgrep значительно опережает ack (0.094 против 0.122 сек) за счет стартовой скорости. Чистые варианты cgrep, hl и ack в директории cexmc незначительно быстрее шелл-функций (0.068 против 0.076 сек в случае cgrep, 0.088 против 0.094 сек в случае hl и 0.120 против 0.122 сек в случае ack) — это говорит о том, что конвейеры, состоящие из программ sed, column и hl, из которых собраны эти функции, достаточно быстры.

понедельник, 22 августа 2016 г.

Универсальные счетчики в nginx: замечания к реализации модуля

Не так давно реализовал универсальные счетчики для nginx в виде модуля. По задумке счетчики могут быть объявлены как в серверной части конфигурации, так и в отдельных локейшнах внутри сервера. Они должны разделяться всеми рабочими процессами, а также, опционально, наборами отдельных виртуальных серверов — это для того, чтобы их можно было опрашивать из отдельного, выделенного сервера. Вот как это может выглядеть на практике.
http {
    server {
        listen       8010;
        server_name  main monitored;

        counter $cnt_all_requests inc;

        location /test {
            counter $cnt_test_requests inc;
            if ($arg_a) {
                counter $cnt_test_a_requests inc;
                echo "/test?a=$arg_a";
                break;
            }
            return 200;
        }
    }

    server {
        listen       8020;
        server_name  monitor monitored;

        allow 127.0.0.1;
        deny  all;

        location / {
            echo -n "all = $cnt_all_requests";
            echo -n " | /test = $cnt_test_requests";
            echo    " | /test?a = $cnt_test_a_requests";
        }
    }
}
Здесь объявлены два виртуальных сервера main и monitor, слушающие на разных портах 8010 и 8020. Второй сервер нужен только для опроса счетчиков, объявленных в первом сервере, поэтому доступ к нему ограничен директивами allow и deny. В первом сервере объявлены три счетчика: cnt_all_requests, cnt_test_requests и cnt_test_a_requests. Первый счетчик должен увеличиваться на единицу (операция inc со значением по умолчанию 1) при любом запросе к серверу main, второй — при попадании в локейшн /test, третий – при попадании в этот же локейшн, но с условием, что параметр a присутствует в URI запроса. Обратите внимание, что оба сервера имеют одинаковое последнее имя monitored — это и есть тот самый способ объявить набор счетчиков, разделяемый несколькими виртуальными серверами (для этой же цели в модуле имеется отдельная директива counter_set_id). Давайте подумаем, как можно было бы реализовать счетчики из этого примера. Во-первых, все рабочие процессы должны разделять одни и те же значения счетчиков, а это значит, что они должны храниться в разделяемой памяти. А как сделать так, чтобы при попадании в локейшн /test счетчики cnt_all_requests и cnt_test_requests увеличивались бы на единицу, а третий счетчик cnt_test_a_requests делал бы это только в том случае, если в URI присутствует параметр a? Для ответа на этот вопрос обратите внимание на выделенное мною в нем слово попадание. В самом деле, попадание запроса в определенный локейшн есть результат большой работы, проделанной nginx на этапе чтения конфигурации, а именно создания и слияния (merge) конфигураций уровня локейшнов (location configuration). При получении запроса nginx сопоставляет его URI с метками всех конфигураций уровня локейшнов и выбирает наиболее подходящую. С помощью механизма слияния конфигураций мы сможем протолкнуть верхнеуровневые конфигурации со счетчиками cnt_all_requests и cnt_test_requests вниз в целевые конфигурации вплоть до уровней if в локейшнах. Таким образом, мы будем хранить ссылки на метаинформацию о счетчиках (ссылку на значение в разделяемой памяти, которое нужно будет изменить, а также операцию — inc или set и соответствующее ей значение) в кофигурации уровня локейшнов нашего модуля. И не стоит удивляться тому, что счетчик, объявленный на уровне server выше всяких локейшнов имеет собственную конфигурацию уровня локейшнов — просто во время чтения конфигурации nginx сольет эту конфигурацию со всеми другими, объявленными на уровне локейшнов и внутри всех if во всех локейшнах. Правила слияния мы запрограммируем сами, гарантировав, что верхнеуровневые счетчики попадут во все конфигурации ниже. А теперь представьте такой фрагмент конфигурации.
        counter $cnt_requests_1 inc;

        location /test {
            counter $cnt_requests_1 inc;
            if ($arg_a) {
                counter $cnt_requests_1 inc -1;
            }
            return 200;
        }
Здесь мы хотим, чтобы при попадании запроса в локейшн /test счетчик cnt_requests_1 увеличивался на 1, а во все другие локейшны — на 2. Вот таким хитроумным способом, эксплуатируя механизм слияния конфигураций уровня локейшнов, мы можем добиться этого результата. Но… Когда мы говорили о слиянии конфигураций, мы имели ввиду простое добавление разных счетчиков сверху вниз. В данном же случае нам придется не просто добавлять новый счетчик, но каким-то образом сливать метаинформацию одного и того же счетчика в нижней конфигурации. К метаинформации счетчика относятся ссылка на элемент в разделяемой памяти (которая не изменится при слиянии), а также операция и ее значение. В общем, нам нужно изменить операцию и/или значение счетчика на нижнем уровне в зависимости от их значений у счетчика на верхнем уровне. И это не сложно. Если операция нижнего счетчика равна set, то метаинформация на нижнем счетчике не меняется, иначе, если операция верхнего счетчика равна set, то операцией нижнего счетчика становится set, а ее значением является сумма значений операций верхнего и нижнего счетчиков, иначе операция нижнего счетчика не изменяется, а ее значением становится сумма значений операций верхнего и нижнего счетчиков. Внимание, вопрос. Когда мы будем обновлять значения счетчиков в разделяемой памяти? Простой вопрос? Как это ни удивительно, но ответ отрицательный. Поскольку мы опираемся на метаинформацию о счетчиках, которая привязана к конфигурации уровня локейшнов, во время обновления данных счетчиков в разделяемой памяти в метаданных запроса nginx (это указатель на объект типа ngx_http_request_t) должна находится правильная ссылка на конфигурацию уровня локейшнов нашего модуля. Запрос nginx проживает насыщенную событиями жизнь, состоящую из нескольких фаз. В течение всей жизни ссылки на конфигурации уровня локейшнов могут меняться несколько раз. Наибольший вклад в этот процесс изменения конфигураций вносит модуль nginx rewrite с его директивами rewrite, if и return. Директивы модуля rewrite действуют на ранних фазах NGX_HTTP_SERVER_REWRITE_PHASE (только те, которые объявлены на уровне серверов, однако в этом случае они не изменяют локейшны) и NGX_HTTP_REWRITE_PHASE (директивы, объявленные на уровне локейшнов). Более того, директива return может убить запрос на одной из этих фаз. Мы могли бы объявить хэндлер, который обновлял бы значения счетчиков на одной из поздних фаз (см. подробную информацию о фазах nginx здесь). Если бы не return… Если объявить наш хэндлер на самой ранней из доступных фаз (NGX_HTTP_REWRITE_PHASE), до запуска хэндлера модуля rewrite, то ссылка на конфигурацию уровня локейшнов может оказаться неверной, поскольку директивы модуля rewrite могут изменить локейшн. Ввиду изложенных обстоятельств я решил поместить хэндлер для обновления счетчиков в фильтр заголовков ответа (response headers filter): он должен работать всегда вне зависимости от наличия или отсутствия директив модуля rewrite и изменять счетчики, связанные с последним переписанным локейшном. Ну а что, если мы хотим регистрировать входные локейшны, которые связаны с запросом до любых изменений локейшнов директивами модуля rewrite? Для этого случая я добавил так называемые ранние счетчики, хэндлеры которых вызываются в фазе NGX_HTTP_REWRITE_PHASE. Вот пример.
        location /test/rewrite {
            early_counter $ecnt_test_requests inc;
            rewrite ^ /test/0;
        }

        location /test/0 {
            early_counter $ecnt_test0_requests inc;
            rewrite ^ /test;
        }
Хэндлер для раннего счетчика ecnt_test_requests в локейшне /test/rewrite будет вызван до директивы rewrite, поэтому данный счетчик увеличится на единицу до любых rewrite и if уровня локейшнов, но это при условии, что локейшн /test/rewrite был входным. Так, в случае запроса на /test/rewrite, счетчик ecnt_test0_requests в промежуточном локейшне /test/0 не изменится — нет никакого способа получить доступ к промежуточным конфигурациям уровня локейшнов во время последовательных перезаписей локейшнов модулем rewrite. Объявления ранних счетчиков разрешены только на уровне локейшнов, поскольку на уровнях серверов, а тем более в if внутри локейшнов они не имеют смысла. Один и тот же счетчик может быть объявлен как обычный и как ранний при условии, что процедура слияния конфигураций уровня локейшнов не выявит разночтений ни в одном случае — флаг признака раннего счетчика хранится в метаинформации счетчика наряду с операцией и значением и не должен изменяться при слиянии счетчиков. И, наконец, к вопросу о реализации разделения счетчиков между виртуальными серверами. Помимо конфигурации уровня локейшнов, nginx позволяет модулям использовать конфигурации уровня серверов (server configuration) и основную конфигурацию (main configuration). Для организации общего доступа к единому набору счетчиков из разных серверов, можно организовать массив таких наборов в основной конфигурации, а в конфигурациях уровня серверов разместить ссылку на определенный элемент из этого массива, и только в том случае, если данный сервер вообще ссылается на какие-либо счетчики. В качестве меток элементов массива наборов счетчиков в основной конфигурации подойдет любая строковая настройка уровня сервера, которая может разделяться разными виртуальными серверами, например последнее имя сервера или специально созданная для этого директива. Чего здесь не хватает? Представьте, что мы хотим увеличивать счетчик когда приходит запрос POST.
http {
    server {
        listen       8010;
        server_name  main monitored;

        if ($request_method = POST) {
            counter $cnt_post_requests inc;
        }
}
Соблазнительно. Но если бы объявления счетчиков и были разрешены внутри серверных if (а это не так), то это все равно не имело бы никакого смысла, поскольку конфигурации уровня локейшнов не доступны внутри серверных if (см. также мою статью о практической бесполезности серверных if для разработки модулей в таких случаях). Это можно исправить с помощью дополнительной переменной.
http {
    server {
        listen       8010;
        server_name  main monitored;

        if ($request_method = POST) {
            set $inc_post_requests 1;
        }

        counter $cnt_post_requests inc $inc_post_requests;
}
А вспомните предыдущий пример со стеком перенаправлений rewrite, в котором невозможно установить счетчик внутри промежуточных локейшнов, таких как /test/0. Если бы операция счетчика могла ссылаться на переменную, мы могли бы устанавливать значение переменной внутри промежуточного локейшна с помощью директивы set, а объявление счетчика перенести на уровень выше, чтобы оно наследовалось в окончательном локейшне после всех перенаправлений rewrite.
        location /test/rewrite {
            early_counter $ecnt_test_requests inc;
            rewrite ^ /test/0 last;
        }

        counter $cnt_test0_requests inc $inc_test0_requests;

        location /test/0 {
            set $inc_test0_requests 1;
            rewrite ^ /test last;
        }
Ссылка на переменную в качестве значения операции счетчика также полезна при задании сложных условий, с которыми директива if справиться не в состоянии несмотря на богатые возможности языка регулярных выражений (см. мои статьи на эту тему здесь и здесь). Вычисление сложного условия можно произвести в коде универсального высокоуровневого языка, такого как Javascript или Perl, когда переменная-значение операции счетчика связана с соответствующим хэндлером, ссылающимся на этот код. Ввиду богатых перспектив такого подхода, ссылка на переменную-значение операции разрешена в объявлении счетчиков. Это немного усложняет вычисление значения операции, поскольку теперь это значение может вычисляться не только в процедуре слияния счетчиков во время чтения конфигурации, но и во время обработки запроса (назовем это время рантаймом, а переменные-значения рантайм-переменными). Приведу пример. Давайте увеличивать счетчик в случае, если некоторое base64-закодированное значение (например, куки Misc), содержит тэг версии типа v=1. Я буду использовать код на языке Haskell из соответствующего модуля nginx. Вот полная конфигурация.
user                    nobody;
worker_processes        4;

events {
    worker_connections  1024;
}

error_log               /tmp/nginx-test-custom-counters-error.log warn;

http {
    default_type        application/octet-stream;
    sendfile            on;

    access_log          /tmp/nginx-test-custom-counters-access.log;

    # uncomment next line on ghc ambiguous interface error
    #haskell ghc_extra_flags '-hide-package regex-pcre';

    haskell compile standalone /tmp/ngx_haskell.hs '

import Data.ByteString.Base64
import Data.Maybe
import Text.Regex.PCRE

hasVTag = either (const False) (isJust . matchOnce r) . decode
    where r = makeRegex "\\\\bv=\\\\d+\\\\b" :: Regex

NGX_EXPORT_B_Y (hasVTag)

    ';

    server {
        listen          8010;
        server_name     main monitored;

        haskell_run hasVTag $hs_inc_cnt_vtag $cookie_misc;
        counter $cnt_cookie_misc_vtag inc $hs_inc_cnt_vtag;

        location / {
            return 200;
        }
    }

    server {
        listen          8020;
        server_name     monitor;
        counter_set_id  monitored;

        allow 127.0.0.1;
        deny  all;

        location / {
            echo "vtag_reqs = $cnt_cookie_misc_vtag";
        }
    }
}
Обратите внимание на то, что в регулярном выражении, переданном в функцию makeRegex, вместо одного обратного слэша используются четыре. Это связано с тем, что как строки Haskell, так и значения в директивах nginx требуют двойного обратного слэша для интерполяции в одинарный. Протестируем.
curl 'http://127.0.0.1:8020/'
vtag_reqs = 0
curl -b 'Misc=bj0yO3Y9MQ==' 'http://127.0.0.1:8010/'
curl 'http://127.0.0.1:8020/'
vtag_reqs = 1
curl -b 'Misc=bj0yO3Y9MQ=' 'http://127.0.0.1:8010/'
curl 'http://127.0.0.1:8020/'
vtag_reqs = 1
Значение bj0yO3Y9MQ== — это base64-закодированная строка n=2;v=1, то же самое значение без последнего символа = — просто сломанная base64-закодированная строка. Так что работает верно. Перейдем к вопросу о реализации разделяемой памяти для счетчиков. В модуле использован стандартный интерфейс nginx (детали его реализации можно найти в этом прекрасном руководстве), включающий вызов функции ngx_shared_memory_add() с размером выделяемой памяти, равным двум страницам (т.е. 2 раза по 4096 байт в Linux). Это минимальный размер сегмента, необходимый слабовому аллокатору ngx_slab_allocator. Каждый набор счетчиков, связанный с одним виртуальным сервером или группой, получает собственный сегмент памяти. Счетчики хранятся в массиве, поскольку на этапе инициализации разделяемой памяти есть возможность подсчитать их общий размер. Каждый элемент массива имеет размер 8 байт на 64-битной машине, первый элемент хранит общее количество счетчиков — эта информация используется при перезагрузке конфигурации, когда nginx получает сигнал HUP, для возможности восстановления старых значений счетчиков. Если слабовый аллокатор использует целую страницу памяти для своих нужд (я пока так и не выяснил этого), то остается целых 4092 байта на счетчики — этого хватит более чем на 500 элементов на один набор. Старые значения счетчиков восстанавливаются после перезагрузки конфигурации, если директива-флаг counters_survive_reload установлена в значение on на уровне сервера или основной конфигурации. Значения счетчиков не восстанавливаются, если их общее количество в наборе изменилось после перезагрузки. Изменение порядка объявления счетчиков в определенном наборе после перезагрузки приведет к тому, что они подхватят значения счетчиков, которые до этого были объявлены на этих позициях. Я не буду приводить исходный код модуля и объяснять значения отдельных строк — надеюсь, что приведенная здесь информация поможет разобраться в нем самостоятельно. Отмечу лишь, что основная, серверная и локейшн-конфигурации модуля соответствуют типам ngx_http_cnt_main_conf_t, ngx_http_cnt_srv_conf_t и ngx_http_cnt_loc_conf_t соответственно, метаинформация счетчиков описана в типе ngx_http_cnt_data_t, а информация, связанная с набором счетчиков — в типе ngx_http_cnt_set_t. В функции ngx_http_cnt_init() происходит инициализация хэндлера фазы NGX_HTTP_REWRITE_PHASE и фильтра заголовков ответа — обе функции вызывают одну и ту же рабочую функцию ngx_http_cnt_update(), в которой происходит изменение значений счетчиков в разделяемой памяти. Функция ngx_http_cnt_get_value() является хэндлером переменной-счетчика и осуществляет доступ к значению в разделяемой памяти.

пятница, 15 июля 2016 г.

C++: загадки инициализации глобальных объектов

Давайте напишем простую структуру, объекты которой смогут выводить на экран время старта программы. Файл pst.h
#include <ctime>

struct ProgramStartTime
{
    ProgramStartTime();

    char *  operator()( void ) const;
    time_t  now_;
};

extern ProgramStartTime  pst_ref;
Файл pst.cc
#include "pst.h"

namespace
{
    time_t  now( time( NULL ) );
}

ProgramStartTime::ProgramStartTime() : now_( now )
{
}

char *  ProgramStartTime::operator()( void ) const
{
    return ctime( &now_ );
}

ProgramStartTime  pst_ref;
Все просто. Время старта программы будем отсчитывать с момента динамической инициализации глобальной переменной now. Функция operator()( void ) структуры ProgramStartTime будет возвращать время now_, инициализированное значением now в конструкторе ProgramStartTime и преобразованное в удобоваримую форму с помощью функции ctime(). Глобальный объект pst_ref будет опорным объектом типа ProgramStartTime в дальнейшем исследовании. А теперь функция main() с собственным глобальным объектом pst. Файл test.cc
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
}
Компилируем и запускаем программу test.
g++ -Wall -o test pst.cc test.cc
./test
          Start time: Fri Jul 15 09:40:11 2016
Reference start time: Fri Jul 15 09:40:11 2016
Все хорошо, не так ли? Да ну! А как вам такое?
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
Ух ты, семидесятый год, начало эпохи! Видимо глобальный объект now был инициализирован нулем! И что самое интересное, мы ведь не меняли исходный код программы, а просто изменили порядок перечисления исходных файлов в списке аргументов g++. Давайте копнем глубже, распечатаем значения членов now_ в объектах pst и pst_ref.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst()
              << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Thu Jan  1 03:00:00 1970
          Plain start time: 0
Plain reference start time: 1468565544
Ага, в объекте pst_ref хранится хорошее время, а в объекте pstноль, то есть начало эпохи. Отложим разбор этого странного факта на потом, пока нужно хотя бы понять, почему функция ctime() в обоих случаях возвращает начало эпохи. Справочное руководство по ctime() говорит, что эта функция возвращает результат в статическом буфере, поэтому она не является потокобезопасной. Нас в данном случае интересует другое. Возвращаемые значения, будучи помещенными несколько раз в один участок памяти в пределах одного выражения (sequence в смысле потока выполнения), могут оказаться одинаковыми: в конце концов это один и тот же участок памяти! Помните известное правило о непредсказуемости порядка вычисления функций в одном выражении? Так вот, в данном случае с функциями, возвращающими результат в глобальном объекте-указателе, это правило распространяется и на возвращаемый результат. Это и есть мораль номер 1. Функции, возвращающие результат вычисления в глобальных объектах, помимо общеизвестных недостатков, таких как небезопасность в многопоточной среде, обладают рядом менее известных проблем, таких как неопределенное поведение с точки зрения возвращаемого значения в пределах одного выражения. Давайте разобьем единое выражение с вызовами ctime() на два и продолжим выяснять, почему в объектах pst и pst_ref хранятся разные значения now_ и как это зависит от порядка перечисления исходных файлов в списке аргументов g++.
#include <iostream>
#include "pst.h"

ProgramStartTime  pst;

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << pst();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << pst.now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Thu Jan  1 03:00:00 1970
Reference start time: Fri Jul 15 10:29:32 2016
          Plain start time: 0
Plain reference start time: 1468567772
Теперь все хорошо с точки зрения соответствия между значениями now_ и вызовами ctime( &now_ ). Но что же не так с самим значением now_ в объекте pst? Обратим внимание на то, что глобальные объекты pst и now определены в разных файлах, то бишь единицах трансляции (translation unit). Вот вам и объяснение и мораль номер 2. Корректная динамическая инициализация зависимых глобальных объектов в разных единицах трансляции не гарантируется. В нашем примере объект pst в лице конструктора его типа зависит от глобального объекта now, динамически (то есть во время выполнения программы) инициализируемого значением, возвращаемым функцией time(). Заметим, что pst тоже инициализируется динамически, и в этом вся соль. Порядок такой инициализации между разными единицами трансляции не определен, несмотря на то, что один из объектов зависит от другого: C++ не отслеживает такого рода зависимости. Зато статическая инициализация глобальных объектов нулями (в том числе и тех, которые в дальнейшем будут инициализированы динамически) происходит раньше любой динамической, вот почему член now_ в объекте pst равен нулю, а не какому-нибудь другому произвольному значению. С другой стороны, объект pst_ref всегда инициализируется корректно. Но это и не удивительно: его определение находится после определения now в той же единице трансляции, а в этом случае стандарт гарантирует последовательную динамическую инициализацию глобальных объектов. В том, что изменение порядка перечисления исходных файлов в списке аргументов g++ повлияло на корректность программы, нет никакого правила: этот эффект нужно рассматривать как особенность конкретного линковщика на конкретной платформе. И, наконец, мораль номер 3 (позитивная). Чтобы избежать эффектов динамической инициализации глобальных объектов, пользуйтесь локальными статическими объектами, возвращаемыми из специально подготовленных для этого функций-оберток. Динамическая инициализация локальных статических объектов происходит после первого обращения к функции-обертке, поэтому, при условии, что эта функция-обертка в первый раз вызывается из функции main() или глубже по стеку, все глобальные объекты, от которых зависит возвращаемый статический объект, будут гарантированно инициализированы до его собственной инициализации. Условие динамической инициализации локальных статических объектов после первого вызова функции, в которой они определены, позволяет настроить цепочку инициализаций зависимых статических объектов (или так называемый dependency tracking) при условии, что все они размещены как локальные статические объекты в собственных функциях-обертках. Я покажу, как в нашем примере обернуть глобальный объект pst (я не стал оборачивать объект now ради формирования цепочки инициализаций, поскольку в этом случае время его инициализации будет совпадать со временем первого вызова функции-обертки и, в принципе, оно может сильно не совпадать со временем старта программы).
#include <iostream>
#include "pst.h"

ProgramStartTime *  getPst( void )
{
    static ProgramStartTime  pst;
    return &pst;
}

int  main( void )
{
    std::cout.width( 22 );
    std::cout << "Start time: " << getPst()->operator()();
    std::cout << "Reference start time: " << pst_ref();
    std::cout.width( 28 );
    std::cout << "Plain start time: " << getPst()->now_ << std::endl
              << "Plain reference start time: " << pst_ref.now_ << std::endl;
}
g++ -Wall -o test test.cc pst.cc
./test
          Start time: Fri Jul 15 11:41:47 2016
Reference start time: Fri Jul 15 11:41:47 2016
          Plain start time: 1468572107
Plain reference start time: 1468572107

среда, 6 июля 2016 г.

Conky: мои настройки отображения погоды (yahooapis и jq) и музыки (cmus)

Оригинальные настройки conky я взял отсюда. В этот пакет, кроме собственно конфигураций окон, входят шрифты для отображения простого текста и погодных символов. К сожалению, в мае этого года окно weather_date перестало показывать погоду. Как выяснилось, это окно использует yahooapis, а Yahoo изменил свой API как раз в это время. В общем, пришлось переписать это окно для новых yahooapis, а заодно улучшить производительность за счет уменьшения вызовов процессов-фильтров, познакомиться с замечательным парсером JSON jq и перевести настройки в новый формат с помощью скрипта convert.lua, который поставляется вместе с обновленным conky. На следующей картинке показано, как выглядит окно weather_date на моих рабочем и домашнем компьютерах.
Запускать conky с окном weather_date нужно так:
conky -c ~/.grayscale/conkyrc/weather_date
А это сам скрипт weather_date.
conky.config = {
--###############
--###############PERFORMANCE_SETTINGS
--###############
    update_interval = 5,
    total_run_times = 0,
    net_avg_samples = 2,
    imlib_cache_size = 0,
    double_buffer = true,
    no_buffers = true,

--###############
--###############TEXT_SETTINGS
--###############
    use_xft = true,
    font = 'GE Inspira:bold:pixelsize=12',
    xftalpha = 0.1,
    override_utf8_locale = true,
    text_buffer_size = 512,

--###############
--###############WINDOW_SPECIFICATIONS
--###############
    background = true,
    own_window = true,
    own_window_transparent = true,
    own_window_type = 'normal',
    own_window_class = 'conky-semi',
    own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
    own_window_argb_visual = true,
    own_window_argb_value = 0,
    draw_outline = false,
--# Window border
    draw_borders = false,
    pad_percents = 0,
    border_inner_margin = 4,
    top_name_width = 10,
    use_spacer = 'right',
--#Size and position
    alignment = 'top_left',
    gap_x = 1600,
    gap_y = 58,
    minimum_width = 0, minimum_height = 0,
    maximum_width = 240,

--###############
--###############GRAPHICS_SETTINGS
--###############
    draw_shades = false,
    default_shade_color = '#292421',
    short_units = true,
--#Default Colors
    default_color = '#efefef',
    default_shade_color = '#1d1d1d',
--#Color Title
    color1 = '#bcbcbc',
    color2 = '#00d787',
    color3 = '#00d787',
};

conky.text = [[
#################
#################DATE & TIME
#################
${voffset 10}${font GE Inspira:pixelsize=50}${color1}${time %H:%M}\
${voffset -20}${offset 5}${font GE Inspira:pixelsize=25}${color2}${time %d}\
${voffset -15}${font GE Inspira:pixelsize=20}${color1}${time  %b}${time %Y}\
${voffset 35}${offset -105}${font GE Inspira:pixelsize=22}${color1}${time %A}\
${color}${font}
${voffset 5}${color3}${hr 2}${color}\

################
################DOWNLOADING WEATHER INFO AND SAVING IT AS ~/.cache/weather.json
################
${execi 1800 nm-online -t 60 && curl -s \
    -G 'http://query.yahooapis.com/v1/public/yql?format=json' \
    --data-urlencode 'q=select * from weather.forecast where woeid in \
    (select woeid from geo.places(1) where text="Saint-Petersburg, Russia") \
    and u="c"' -o ~/.cache/weather.json}
################MAIN WEATHER IMAGE
${voffset -10}${offset 20}${font conkyweather:size=140}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.condition.code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}
################WEATHER CONDITIONS
${alignc 10}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.condition.text' \
    ~/.cache/weather.json}${color}${font}
##############EXTRACTING CURRENT/HIGH TEMP IN DEGREE CELSIUS
${offset 30}${font GE Inspira:pixelsize=50}${color3}\
${execi 1800 jq -r '.query.results.channel.item.condition.temp' \
    ~/.cache/weather.json}°C/${font GE Inspira:pixelsize=30}\
${color3}${execi 1800 jq -r '.query.results.channel.item.forecast[0].high' \
    ~/.cache/weather.json}°C\
${color}${font}\

#################
#################EXTRACTING LOCATION
#################
${voffset 16}${offset 16}${font GE Inspira:bold:pixelsize=20}${color1}\
${execi 1800 jq -j '.query.results.channel.location | .city + ", ", .country' \
    ~/.cache/weather.json}${color}${font}
${color3}${hr 2}${color}\

#################
#################EXTRACTING WEATHER INFO
#################
##PRESSURE     HUMIDITY
${voffset 5}${font GE Inspira:bold:pixelsize=12}${color2}\
Pressure : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.pressure' \
    ~/.cache/weather.json}mb\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Humidity : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.humidity' \
    ~/.cache/weather.json}%\
${color}${font}
##SUNRISE     SUNSET
${font GE Inspira:bold:pixelsize=12}${color2}\
Sunrise : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunrise' \
    ~/.cache/weather.json}\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Sunset : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunset' \
    ~/.cache/weather.json}${color}${font}
##WIND     VISIBILITY
${font GE Inspira:bold:pixelsize=12}${color2}\
Wind : ${color1}${execi 1800 jq -r '.query.results.channel.wind.speed' \
    ~/.cache/weather.json}km/hr\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Visibility : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.visibility' \
    ~/.cache/weather.json}km${color}${font}
${color3}${hr 2}${color}\

#################
#################WEATHER FORECAST IMAGES FOR NEXT 2 DAYS
#################
${voffset 10}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[1].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${tab 72}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[2].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}\

#################
#################EXTRACTING LOW/HIGH TEMP IN DEGREE CELSIUS FOR NEXT 2 DAYS
#################
${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].high' \
    ~/.cache/weather.json}°\
${alignr -16}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].high' \
    ~/.cache/weather.json}°${color}${font}
${voffset 5}${color3}${hr 2}${color}\

#################
#################CALENDER DISPLAY
#################
${voffset 10}${font nimbus mono L:bold:size=12}${color1}\
${execpi 1800 DJS=`date +%_d`; cal | \
    sed -e s/"\(^\|[^0-9]\)$DJS"'\b'/'\1${color2}'"$DJS"'$color'/ -e s/^/'  '/}
]];
Внутри conky.config находятся настройки геометрии окна, объявляются различные цвета и т.п. Собственно содержимое окна описано внутри conky.text. В параграфе, озаглавленном как DOWNLOADING WEATHER …, каждые полчаса (1800 секунд) с помощью execi и curl, после предварительной проверки доступности сети командой nm-online, выполняется запрос на query.yahooapis.com в формате YQL (Yahoo! Query Language). Ответ от Yahoo сохраняется в файле ~/.cache/weather.json. Обратите внимание на то, что данная команда execi ничего не выводит на экран, то есть фактически в данном случае conky работает как cron! Я выбрал формат вывода JSON потому, что он легко и непринужденно парсится прямо из командной строки командой jq, которая позволяет задавать довольно сложные фильтры для поиска данных, предоставляя своеобразный язык запросов, который очень подробно, с примерами, описан в man jq. Все оставшиеся параграфы внутри conky.text вплоть до вывода календаря вызывают jq для поиска значений определенных погодных категорий внутри файла ~/.cache/weather.json. Например, внутри параграфа EXTRACTING LOCATION выполняется запрос jq -j '.query.results.channel.location | .city + ", ", .country' ~/.cache/weather.json, который выводит значения city и country, находящиеся в пути /query/results/channel/location структуры JSON и выводит их в одной строке (опция -j). Поскольку jq работает из командной строки, имеется возможность выводить погодные условия прямо на терминал! Например, погоду на завтра можно вывести командой
jq '.query.results.channel.item.forecast[1]' ~/.cache/weather.json
{
  "code": "12",
  "date": "07 Jul 2016",
  "day": "Thu",
  "high": "17",
  "low": "12",
  "text": "Rain"
}
При этом вывод в терминале будет синтаксически подсвечен! А теперь о выводе информации из cmus. Cmus — это аудиоплеер для терминала. В оригинальном пакете была представлена поддержка плеера clementine, но я им не пользуюсь. Информация из clementine выводилась в окне net_hdd с помощью execpi 2 ~/.grayscale/data/clementine, скрипт clementine в директории ~/.grayscale/data/ был написан на perl. Сначала я написал аналогичный скрипт cmus, тоже на perl, но в новом conky большое количество проблем заставило переписать его на lua и загружать в conky командой lua_load. Вот так выглядит информация из cmus, когда он запущен.
Для того, чтобы это заработало, в net_hdd, предварительно обработанном скриптом convert.lua, внутрь conky.config нужно добавить строку
        lua_load = '~/.grayscale/data/cmus.lua',
Внутрь conky.text, вместо настроек для clementine, нужно добавить строки
################
################CMUS_DISPLAY
################
${voffset 5}${offset 22}\
${font GE Inspira:bold:pixelsize=15}${color2}CMUS${voffset 2}\
${offset 5}${color3}${hr 2}${color}${font}
${if_running cmus}${lua_parse cmus}${else}${voffset 46}${endif}
Обратите внимание, что в случае, когда cmus не запущен, в окно net_hdd выводится пустой вертикальный сдвиг высотой 46 пикселей: это сделано для того, чтобы окно не мерцало при запуске и закрытии cmus. Величина сдвига была определена экспериментально: она должна точно соответствовать высоте совокупной информации из cmus, когда он запущен, выводимой conky. Кроме того, стоит отметить, что lua_parse, в отличие от execpi, не имеет собственной настройки интервала обновления данных, а следовательно скорость обновления данных определяется системной настройкой окна update_interval внутри conky.config. Скрипт cmus.lua в директории ~/.grayscale/data/ выглядит так.
function conky_echo(a)
    return a
end

function conky_cmus()
    local artist   = 'N/A';
    local title    = 'N/A';
    local album    = 'N/A';
    local progress = 0;
    local pos      = 0;
    local length   = 0;
    local status   = '';
    
    local color1   = 'bcbcbc';
    local color2   = 'ffa300';
    local color3   = 'ffff5f';
    
    f = assert( io.popen( 'cmus-remote -Q' ) ) or os.exit( 1 )
     
    for line in f:lines() do
        local v = string.match( line, '^status%s*(.*)' )
        if v ~= nil and v ~= '' and v ~= 'playing' then
            status = ' [' .. v .. ']'; goto next
        end
        v = string.match( line, '^duration%s*(.*)' )
        if v ~= nil and v ~= '' then length = tonumber( v ); goto next end
        v = string.match( line, '^position%s*(.*)' )
        if v ~= nil and v ~= '' then pos = tonumber( v ); goto next end
        v = string.match( line, '^tag album%s*(.*)' )
        if v ~= nil and v ~= '' then album = v; goto next end
        v = string.match( line, '^tag artist%s*(.*)' )
        if v ~= nil and v ~= '' then artist = v; goto next end
        v = string.match( line, '^tag title%s*(.*)' )
        if v ~= nil and v ~= '' then title = v; goto next end
        ::next::
    end
      
    f:close()
    
    if pos > 0 and length > 0 then
        progress = math.floor( 100 * pos / length )
    end
    
    return '${voffset 5}${offset 6}${font StyleBats:size=10}${color ' ..
            color1 .. '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Title: ${color ' .. color3 .. '}${alignr}' .. title ..
            '${color ' .. color2 .. '}' .. status ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Artist: ${color ' .. color3 .. '}${alignr}' .. artist ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Album: ${color ' .. color3 .. '}${alignr}' .. album ..
            '${color ' .. color1 .. '}${font}' ..
            '\n${voffset 1}${offset 8}${lua_bar echo ' .. progress .. '}'
end
Прошу меня простить, если что-то здесь сделано неизящно или неэффективно — это моя первая программа на lua :)