ngx-export-tools-extra-0.8.2.0: More extra tools for Nginx haskell module
Copyright(c) Alexey Radkov 2021
LicenseBSD-style
Maintaineralexey.radkov@gmail.com
Stabilityexperimental
Portabilitynon-portable (requires Template Haskell)
Safe HaskellNone
LanguageHaskell2010

NgxExport.Tools.ServiceHookAdaptor

Description

A service hook adaptor from the more extra tools collection for nginx-haskell-module.

Synopsis

    Maintaining custom global data in run-time

    This module exports a simple service (in terms of module NgxExport.Tools) simpleService_hookAdaptor which sleeps forever. Its sole purpose is to serve service hooks for changing global data in all the worker processes in run-time. A single service hook adaptor can serve any number of service hooks with any type of global data.

    Below is a simple example.

    File test_tools_extra_servicehookadaptor.hs

    {-# LANGUAGE TemplateHaskell, OverloadedStrings #-}
    
    module TestToolsExtraServiceHookAdaptor where
    
    import           NgxExport
    import           NgxExport.Tools.ServiceHookAdaptor ()
    
    import           Data.ByteString (ByteString)
    import qualified Data.ByteString as B
    import qualified Data.ByteString.Lazy as L
    import           Data.IORef
    import           System.IO.Unsafe
    
    secretWord :: IORef ByteString
    secretWord = unsafePerformIO $ newIORef ""
    {-# NOINLINE secretWord #-}
    
    testSecretWord :: ByteString -> IO L.ByteString
    testSecretWord v = do
        s <- readIORef secretWord
        return $ if B.null s
                     then "null"
                     else if v == s
                              then "set"
                              else "unset"
    ngxExportIOYY 'testSecretWord
    
    changeSecretWord :: ByteString -> IO L.ByteString
    changeSecretWord s = do
        writeIORef secretWord s
        return "The secret word was changed"
    ngxExportServiceHook 'changeSecretWord
    

    Here we are going to maintain a secret word of type ByteString in run-time. When a worker process starts, the word is empty. The word can be changed in run-time by triggering service hook changeSecretWord. Client requests are managed differently depending on their knowledge of the secret which is tested in handler testSecretWord.

    File nginx.conf

    user                    nobody;
    worker_processes        2;
    
    events {
        worker_connections  1024;
    }
    
    error_log               /tmp/nginx-test-haskell-error.log info;
    
    http {
        default_type        application/octet-stream;
        sendfile            on;
        error_log           /tmp/nginx-test-haskell-error.log;
        access_log          /tmp/nginx-test-haskell-access.log;
    
        haskell load /var/lib/nginx/test_tools_extra_servicehookadaptor.so;
    
        haskell_run_service simpleService_hookAdaptor $hs_hook_adaptor '';
    
        haskell_service_hooks_zone hooks 32k;
    
        server {
            listen       8010;
            server_name  main;
    
            location / {
                haskell_run testSecretWord $hs_secret_word $arg_s;
    
                if ($hs_secret_word = null) {
                    echo_status 503;
                    echo "Try later! The service is not ready!";
                    break;
                }
    
                if ($hs_secret_word = set) {
                    echo_status 200;
                    echo "Congrats! You know the secret word!";
                    break;
                }
    
                echo_status 404;
                echo "Hmm, you do not know a secret!";
            }
    
            location /change_sw {
                allow 127.0.0.1;
                deny all;
    
                haskell_service_hook changeSecretWord $hs_hook_adaptor $arg_s;
            }
        }
    }
    

    Notice that service simpleService_hookAdaptor is not shared, however this is not such important because shared services must work as well.

    A simple test

    After starting Nginx, the secret word service must be not ready.

    $ curl 'http://127.0.0.1:8010/'
    Try later! The service is not ready!

    Let's change the secret word,

    $ curl 'http://127.0.0.1:8010/change_sw?s=secret'

    and try again.

    $ curl 'http://127.0.0.1:8010/'
    Hmm, you do not know a secret!
    $ curl 'http://127.0.0.1:8010/?s=try1'
    Hmm, you do not know a secret!
    $ curl 'http://127.0.0.1:8010/?s=secret'
    Congrats! You know the secret word!

    Change the secret word again.

    $ curl 'http://127.0.0.1:8010/change_sw?s=secret1'
    $ curl 'http://127.0.0.1:8010/?s=secret'
    Hmm, you do not know a secret!
    $ curl 'http://127.0.0.1:8010/?s=secret1'
    Congrats! You know the secret word!

    What if a worker process quits for some reason or crashes? Let's try!

    # ps -ef | grep nginx | grep worker
    nobody     13869   13868  0 15:43 ?        00:00:00 nginx: worker process
    nobody     13870   13868  0 15:43 ?        00:00:00 nginx: worker process
    # kill -QUIT 13869 13870
    # ps -ef | grep nginx | grep worker
    nobody     14223   13868  4 15:56 ?        00:00:00 nginx: worker process
    nobody     14224   13868  4 15:56 ?        00:00:00 nginx: worker process
    $ curl 'http://127.0.0.1:8010/?s=secret1'
    Congrats! You know the secret word!

    Our secret is still intact! This is because service hooks manage new worker processes so well as those that were running when a hook was triggered.