ngx-export-tools-extra-1.2.8.1: More extra tools for Nginx Haskell module
Copyright(c) Alexey Radkov 2021-2024
LicenseBSD-style
Maintaineralexey.radkov@gmail.com
Stabilitystable
Portabilitynon-portable (requires Template Haskell)
Safe HaskellSafe-Inferred
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) 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           Control.Monad
    import           Control.Exception
    import           System.IO.Unsafe
    
    data SecretWordUnset = SecretWordUnset
    
    instance Exception SecretWordUnset
    instance Show SecretWordUnset where
        show = const "unset"
    
    secretWord :: IORef ByteString
    secretWord = unsafePerformIO $ newIORef ""
    {-# NOINLINE secretWord #-}
    
    testSecretWord :: ByteString -> IO L.ByteString
    testSecretWord v = do
        s <- readIORef secretWord
        when (B.null s) $ throwIO SecretWordUnset
        return $ if v == s
                     then "success"
                     else ""
    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 noarg;
    
        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 = unset) {
                    echo_status 503;
                    echo "Try later! The service is not ready!";
                    break;
                }
    
                if ($hs_secret_word = success) {
                    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.

    Note, however, that the order of service hooks execution in a restarted worker process is not well-defined which means that hooks that affect the same data should be avoided. For example, we could declare another service hook to reset the secret word.

    File test_tools_extra_servicehookadaptor.hs: reset the secret word

    resetSecretWord :: ByteString -> IO L.ByteString
    resetSecretWord = const $ do
        writeIORef secretWord ""
        return "The secret word was reset"
    ngxExportServiceHook 'resetSecretWord
    

    File nginx.conf: new location /reset_sw in server main

            location /reset_sw {
                allow 127.0.0.1;
                deny all;
    
                haskell_service_hook resetSecretWord $hs_hook_adaptor;
            }
    

    Both changeSecretWord and resetSecretWord alter the secretWord storage. The order of their execution in a restarted worker process may differ from the order they had happened before the new worker started, and therefore the state of secretWord can get altered in the new worker.

    To fix this issue in this example, get rid of hook resetSecretWord and use directive rewrite to process the reset request in location /change_sw.

            location /reset_sw {
                allow 127.0.0.1;
                deny all;
    
                rewrite ^ /change_sw last;
            }
    

    You may also want to change the hook message in changeSecretWord to properly log the reset case.

    changeSecretWord :: ByteString -> IO L.ByteString
    changeSecretWord s = do
        writeIORef secretWord s
        return $ "The secret word was " `L.append` if B.null s
                                                       then "reset"
                                                       else "changed"