Copyright | (c) Alexey Radkov 2021-2024 |
---|---|
License | BSD-style |
Maintainer | alexey.radkov@gmail.com |
Stability | stable |
Portability | non-portable (requires Template Haskell) |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
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"