client side notification on table change in Common Lisp with Postmodern package

Viewed 230

While googling and looking for examples for quite a while, I could not find any indication as to whether or not it is possible to get a notification "callback" in client applications, when a database table changes.

The feature is supported (somewhat at least) in the postgresql C library, so the database itself seems to support this use case.

CREATE TABLE IF NOT EXISTS disco (
instance TEXT NOT NULL,
service TEXT NOT NULL,
attributes TEXT[] DEFAULT '{}',
endpoints TEXT[],
CONSTRAINT service_instance PRIMARY KEY(instance,service));

Based on the table given above, I created a small Lisp package, which allows advertising some services (it is a toy service discovery, backed by a database), using the postmodern postgresql package.

Of course, aside from advertising, applications will also want to wait for / search for active services. And since polling is clearly not the way to go, some form of notification is needed. On C API level, applications can use select() to wait for the socket, connected to the database and PQNotify() or something like that.

My question is, if postmodern also supports this use case and I am looking for an example.

Here the LISP package as I did it so far.

(ql:quickload :postmodern)
(defpackage :disco
  (:use :common-lisp :postmodern)
  (:export
   :connect-toplevel
   :ensure-disco-table
   :advertise
   :stop-advertise
   :advertised)
  (:shadow :connect-toplevel))
(in-package :disco)
(defun ensure-disco-table ()
  (postmodern:query "CREATE TABLE IF NOT EXISTS disco (
instance TEXT NOT NULL,
service TEXT NOT NULL,
attributes TEXT[] DEFAULT '{}',
endpoints TEXT[],
CONSTRAINT service_instance PRIMARY KEY(instance,service));"))
(defun connect-toplevel ()
  (progn
    (postmodern:connect-toplevel "postgres" "postgres" "password" "localhost")
    (ensure-disco-table)))
(defun advertise (instance service endpoints &optional attributes)
  (let ((attribs (if attributes attributes #())))
      (postmodern:query (:insert-into 'disco :set 'instance instance
                                      'service service
                                      'attributes attribs
                                      'endpoints endpoints))))

(defun stop-advertise (instance service)
  (postmodern:query (:delete-from 'disco
                                         :where (:and
                                                 (:= 'instance instance)
                                                 (:= 'service service)))))

(defun advertised (instance service endpoints handler &optional attributes)
  (if (advertise instance service endpoints attributes)
      (progn
        (funcall handler)
        (stop-advertise instance service)
        T)
      nil))

For the searching part, I fancy a function similar to:

(defun with-services (filters continuation)
  ; ...
)

Where filters allows to look for e.g. service names and attributes (does not matter for the question, really) and once all required services are available, the continuation function is called with a list of rows of the relevant services.

The with-services function can either block until every service needed is there or it could do it asynchronously.

Ideas, if this is possible without extending the postmodern package? How would it look like?

Update

Since I could not find documentation I steeped to source code level and in file protocol.lisp I found how notifications are being handled:

(defun get-notification (socket)
  "Read an asynchronous notification message from the socket and
signal a condition for it."
  (let ((pid (read-int4 socket))
        (channel (read-str socket))
        (payload (read-str socket)))
    (warn 'postgresql-notification
          :pid pid
          :channel channel
          :payload payload
          :format-control "Asynchronous notification ~S~@[ (payload: ~S)~]
                           received from ~ server process with PID ~D."
          :format-arguments (list channel payload pid))))

So it appears to my not yet well trained LISP eyes, that notifications are somehow mapped to some sort of exceptions. In other languages, this is usually a code smell. Does this mean, that notification handling is a hidden non-feature or is this an idiomatic way to do things in Common Lisp?

1 Answers

Common Lisp conditions aren't exceptions, since signaling them doesn't unwind the stack. This is a natural use of the Common Lisp condition system, unachievable by popular throw-only exception systems of C++/Java/Python.

The function WARN signals a condition, allowing condition handlers that match the condition type POSTGRESQL-NOTIFICATION to fire and then invoke the MUFFLE-WARNING restart to continue execution. WARN is used here to print a warning message to standard output in case the warning is not handled by MUFFLE-WARNING.

Let us consider the following code:

(labels ((handle-notification-p (condition)
           (let ((pid (postgresql-notification-pid condition))
                 (channel (postgresql-notification-channel condition))
                 (payload (postgresql-notification-payload condition)))
             ...)) ;; return true if we want to handle this notification
         (handle-notification (condition)
           (when (handle-notification-p condition)
             ... ;; perform actions to handle the notifications
             (invoke-restart 'muffle-warning condition))))
  (handler-bind ((postgresql-notification #'handle-notification))
    ...)) ;; application logic goes here

This code establishes a handler that will handle a notification condition. It has three places to fill in.

The first one should return true iff the condition should be handled by this handler. The second one - what steps should be performed to handle the condition before returning control to the warning site. The third one should contain application logic that contains some of our server code that handles communication with Postgres.

All of this happens without unwinding the stack (unless some other condition handler explicitly performs a non-local exit elsewhere). Execution will continue from the WARN site if the notification was handled, and it will continue with a warning printout to standard output if it wasn't.

Related