Shadowing special variables in threaded handlers

Chun Tian binghe.lisp at gmail.com
Fri Dec 31 09:09:08 UTC 2021


Hi,

Imagine the following simple "algorithm" (or strategy): "shadowed values always have priority, otherwise the current value of global variables are used."  Problem is that, how could the lambda function know it was called with a shadowed value? An easy way is to use another global variable which should be never changed by SETQ:

So consider the following modified examples:

(defvar *foo* "original value")
(defvar *shadowed* nil)

(defun create-server (&optional (port 1965))
  (usocket:socket-server "0.0.0.0" port
                         (let ((foo *foo*) (shadowed *shadowed*))
                           (lambda (stream)
                             (let ((v (if shadowed foo *foo*)))
                               (write v :stream stream))))
                         ()
                         :multi-threading t
                         :element-type 'character
                         :in-new-thread t))

(defparameter *server1*
  (let ((*foo* "shadowed value")
        (*shadowed* t))
    (create-server 1965)))

(defparameter *server2* (create-server 1966))

There are two "servers" listening on port 1965 and 1966. Now I have:

$ telnet 127.0.0.1 1965
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
"shadowed value"

$ telnet 127.0.0.1 1966
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
"original value"

After I changed the value of *foo* by (SETQ *FOO* "new value"):

$ telnet 127.0.0.1 1965
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
"shadowed value"

$ telnet 127.0.0.1 1966
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
"new value"

But note that, in general, if you change the value of a global variable from one thread, the change may not be immediately visible from another thread, unless you use something like locks (or atomic updates) from multi-threading libraries. The related issue is not in scope of the socket library.

Regards,

Chun Tian

> On Dec 31, 2021, at 05:48, Duncan Bayne <duncan at bayne.id.au> wrote:
> 
> 
> Chun Tian writes:
> 
>> The difference here is that now (lambda (stream) ...) is a closure
>> which will contain a local version of *foo* at the time when
>> (create-server) is called.  This kind of uses of lambda functions is
>> like a cheap object with a member variable.
> 
> Thanks for the suggestion - yes, this does work as expected, but
> introduces a difficulty with the API of the library.
> 
> The germinal code is as follows (edited to remove large swathes and just
> focus on the relevant bits):
> 
> =====
> (defvar *germinal-cert* "/etc/germinal/cert.pem")
> (defvar *germinal-cert-key* "/etc/germinal/key.pem")
> (defvar *germinal-tls-context* nil "Variable used to store global TLS context")
> 
> ;; snip
> 
>  (with-global-context (*germinal-tls-context* :auto-free-p (not background))
>     (usocket:socket-server host port #'gemini-handler ()
>                           :multi-threading t
>                           :element-type '(unsigned-byte 8)
>                           :in-new-thread background)))
> 
> ;; snip
> 
> (defun gemini-handler (stream)
>  "The main Gemini request handler. Sets up TLS and sets up request and response"
>  (handler-case
>      (let* ((tls-stream (make-ssl-server-stream stream
>                                                 :certificate *germinal-cert*
>                                                 :key *germinal-cert-key*))
> ;; snip
> =====
> 
> So replacing the handler function with a lambda that creates a closure
> works ... but breaks the non-testing case where you just want to setq
> the special variables in your app startup and be done with it.
> 
> The best approach I can think of is something like ...
> 
> =====
> ;; snip
>  (with-global-context (*germinal-tls-context* :auto-free-p (not background))
>     (usocket:socket-server host port
>                           (let ((*threaded-cert* *germinal-cert*)
>                                 (*threaded-cert-key* *germinal-cert-key*))
>                                   (lambda (stream) (gemini-handler stream)))
> ;; snip
>      (let* ((tls-stream (make-ssl-server-stream stream
>                                                 :certificate *threaded-cert*
>                                                 :key *threaded-cert-key*))
> ;; snip
> =====
> 
> Which seems weird, but also gives the best of both worlds; the ability
> to shadow variables for testing purposes, but also setq the *same*
> variables for global configuration.
> 
> Thoughts / opinions?
> 
> --
> Duncan Bayne
> +61 420 817 082 | https://duncan.bayne.id.au/
> 
> I usually check my mail every 24 - 48 hours.  If there's something
> urgent going on, please send me an SMS or call me.

-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: Message signed with OpenPGP
URL: <https://mailman.common-lisp.net/pipermail/usocket-devel/attachments/20211231/58ddf432/attachment-0001.sig>


More information about the usocket-devel mailing list