luv

Workshop wiki

client.lisp

lobby/client.lisp

system luv/lobby · 47 definitions · on GitHub

A reusable application lobby radio.

One worker owns the transport connection. Applications and their render threads see only immutable semantic snapshots and retained values copied under a small mutex; they never perform socket I/O.

in-package#:luv.lobby
defparameter+lobby-presence-prefix+

luv/presence/

defparameter+lobby-presence-filter+

luv/presence/+

defparameter+lobby-store-prefix+

luv/store/

defparameter+lobby-store-filter+

luv/store/+

defparameter+lobby-offline-payload+
string#\Null
defvar*lobby-client-counter-lock*
sb-thread:make-mutex:name"lobby client identities"
defclasslobby-peer
id:initarg:id:readerlobby-peer-id
name:initarg:name:readerlobby-peer-name
:documentation

One application instance heard on the current connection.

defstruct
lobby-snapshot
:constructormake-lobby-snapshot
&keystatuspeerslast-errorrevision

An immutable, connection-scoped view suitable for a frame boundary.

statuspeerslast-errorrevision
defclasslobby-transport
:documentation

The socket-owning edge used by one lobby-client worker.

defgenericopen-lobby-transport
transportclient
:documentation

Open and return client's worker-owned connection.

defgenericsubscribe-lobby-transport
transportconnectionclient
:documentation

Subscribe connection to client's shared lobby topics.

defgenericpublish-lobby-transport
transportconnectionclientpayload
:documentation

Publish client's retained presence payload.

defgenericnext-lobby-publication
transportconnectionclient
:documentation

Return TOPIC and PAYLOAD for one publication, or NIL on a bounded idle poll.

defgenericclose-lobby-transport
transportconnectionclient
:documentation

Close connection without allowing an error to escape cleanup.

defclassmqtt-lobby-transport
connect-timeout:initarg:connect-timeout:initform1/4:readermqtt-lobby-connect-timeout
poll-timeout:initarg:poll-timeout:initform1/4:readermqtt-lobby-poll-timeout
keep-alive:initarg:keep-alive:initform20:readermqtt-lobby-keep-alive
:documentation

The MQTT lobby transport. Every blocking operation is owned by the worker and bounded tightly enough for cooperative application shutdown.

defclasslobby-client
id:initarg:id:readerlobby-client-id
name:initarg:name:readerlobby-client-name
client-id-prefix:initarg:client-id-prefix:readerlobby-client-id-prefix
transport:initarg:transport:readerlobby-client-transport
lock:initform
sb-thread:make-mutex:name"lobby semantic state"
:readerlobby-client-lock
wake:initform
sb-thread:make-waitqueue:name"lobby worker wake"
:readerlobby-client-wake
status:initform:stopped:accessorlobby-client-status
last-error:initformnil:accessorlobby-client-last-error
peers:initform
make-hash-table:test#'equal
:readerlobby-client-peers
values:initform
make-hash-table:test#'equal
:readerlobby-client-values
revision:initform0:accessorlobby-client-revision
connection:initformnil:accessorlobby-client-connection
stopping-p:initformnil:accessorlobby-client-stopping-p
thread:initformnil:accessorlobby-client-thread
:documentation

A restartable radio whose worker publishes connection-scoped snapshots.

define-conditionlobby-worker-stop-timeout
error
client:initarg:client:readerlobby-worker-stop-timeout-client
seconds:initarg:seconds:readerlobby-worker-stop-timeout-seconds
:report
lambda
conditionstream
formatstream"Lobby worker for ~A did not stop within ~,2F seconds."
lobby-client-name
lobby-worker-stop-timeout-clientcondition
lobby-worker-stop-timeout-secondscondition
defmethodprint-object
stream
print-unreadable-object
clientstream:typet:identityt
formatstream"~A ~(~A~)"
lobby-client-nameclient
lobby-client-statusclient
defunlobby-topic-prefix-p
prefixtopic
and
<=
lengthprefix
lengthtopic
string=prefixtopic:end2
lengthprefix
defunlobby-safe-id
text
string-downcase
with-output-to-string
stream
loopforcharacteracrosstextdo
write-char
if
alphanumericpcharacter
character#\-
stream
defundefault-lobby-participant-name
or
let
name
uiop:getenv"LUV_PLAYER_NAME"
andname
plusp
lengthname
name
let
name
uiop:getenv"USER"
andname
plusp
lengthname
string-capitalizename
ignore-errors
machine-instance
"someone"
defunmake-lobby-client
&key
client-id-prefix"luv"
transport
make-instance'mqtt-lobby-transport

Make a stopped lobby client. start-lobby-client owns all transport I/O.

let*
prefix
lobby-safe-idclient-id-prefix
machine
lobby-safe-id
or
ignore-errors
machine-instance
"machine"
id
formatnil"~A-~A-~D-~D"prefixmachine
sb-posix:getpid
make-instance'lobby-client:namename:idid:client-id-prefixprefix:transporttransport
defunlobby-client-topic
client
concatenate'string+lobby-presence-prefix+
lobby-client-idclient
defun%clear-lobby-cache
client
clrhash
lobby-client-peersclient
clrhash
lobby-client-valuesclient
defun%publish-lobby-status
clientstatus&keyconditionclear-cache-p

Publish status while CLIENT-LOCK is held.

let*
detail
andcondition
princ-to-stringcondition
changed-p
or
not
eqstatus
lobby-client-statusclient
not
equaldetail
lobby-client-last-errorclient
andclear-cache-p
or
plusp
hash-table-count
lobby-client-peersclient
plusp
hash-table-count
lobby-client-valuesclient
setf
lobby-client-statusclient
status
lobby-client-last-errorclient
detail
whenclear-cache-p
whenchanged-p
incf
lobby-client-revisionclient
status
defunpublish-lobby-status
clientstatus&keyconditionclear-cache-p
sb-thread:with-mutex
lobby-client-lockclient
%publish-lobby-statusclientstatus:conditioncondition:clear-cache-pclear-cache-p
defunlobby-client-running-p
client
andclient
sb-thread:with-mutex
lobby-client-lockclient
let
thread
lobby-client-threadclient
andthread
sb-thread:thread-alive-pthread
not
lobby-client-stopping-pclient
defunlobby-client-stop-requested-p
client
sb-thread:with-mutex
lobby-client-lockclient
lobby-client-stopping-pclient
defunlobby-client-snapshot
client

Copy client's small semantic state atomically; never touches its transport.

ifclient
sb-thread:with-mutex
lobby-client-lockclient
make-lobby-snapshot:status
lobby-client-statusclient
:peers
sort
loopforpeerbeingthehash-valuesof
lobby-client-peersclient
collectpeer
#'string-lessp:key#'lobby-peer-name
:last-error
lobby-client-last-errorclient
:revision
lobby-client-revisionclient
make-lobby-snapshot:status:stopped:peersnil:last-errornil:revision0
defgenericlobby-client-summary
client
:documentation

Return client's STATUS, PEER-COUNT, LAST-ERROR, and REVISION as values.

This is the constant-work frame-boundary view: it neither copies nor sorts the peer collection and never touches the transport. Detailed tools which need peer identities continue to use lobby-client-snapshot.

defmethodlobby-client-summary
sb-thread:with-mutex
lobby-client-lockclient
values
lobby-client-statusclient
hash-table-count
lobby-client-peersclient
lobby-client-last-errorclient
lobby-client-revisionclient
defmethodlobby-client-summary
clientnull
declare
ignoreclient
values:stopped0nil0
defunlobby-client-value
clientkey

Return the current connection's retained value for key, or NIL.

whenclient
sb-thread:with-mutex
lobby-client-lockclient
gethashkey
lobby-client-valuesclient
defgenericreceive-lobby-publication
clienttopicpayload
:documentation

Publish one transport TOPIC/PAYLOAD into client's semantic local state.

defmethodreceive-lobby-publication
topicpayload
cond
let
unless
string=id
lobby-client-idclient
sb-thread:with-mutex
lobby-client-lockclient
let*
peers
lobby-client-peersclient
old
gethashidpeers
tombstone-p
or
zerop
lengthpayload
cond
andtombstone-pold
remhashidpeers
incf
lobby-client-revisionclient
and
nottombstone-p
or
nullold
not
string=payload
lobby-peer-nameold
setf
gethashidpeers
make-instance'lobby-peer:idid:namepayload
incf
lobby-client-revisionclient
let
sb-thread:with-mutex
lobby-client-lockclient
let*
values
lobby-client-valuesclient
old
gethashkeyvalues
tombstone-p
zerop
lengthpayload
cond
andtombstone-pold
remhashkeyvalues
incf
lobby-client-revisionclient
and
nottombstone-p
not
equaloldpayload
setf
gethashkeyvalues
payload
incf
lobby-client-revisionclient
client
defmethodopen-lobby-transport
let
mqtt.net:open-lobby-connection:client-id
lobby-client-idclient
:keep-alive
mqtt-lobby-keep-alivetransport
:timeout
mqtt-lobby-connect-timeouttransport

The broker rejects a zero-byte Will even though MQTT permits it. A NUL is the retained radio tombstone; graceful shutdown uses empty.

:will
make-instance'mqtt:will:topictopic:payload+lobby-offline-payload+:qos1:retaint
defmethodsubscribe-lobby-transport
connection
declare
ignoretransportclient
mqtt.net:subscribeconnection
defmethodpublish-lobby-transport
connectionpayload
declare
ignoretransport
mqtt.net:publishconnectionpayload:qos1:retaint
defmethodnext-lobby-publication
connection
declare
ignoreclient
handler-case
let
message
mqtt.net:next-messageconnection:timeout
mqtt-lobby-poll-timeouttransport
whenmessage
values
mqtt:publish-topicmessage
mqtt:publish-payload-stringmessage
mqtt.net:connection-timeout
nil
defmethodclose-lobby-transport
connection
declare
ignoretransportclient
mqtt.net:close-mqtt-connectionconnection
defunwait-before-lobby-retry
clientseconds
sb-thread:with-mutex
lobby-client-lockclient
unless
lobby-client-stopping-pclient
sb-thread:condition-wait
lobby-client-wakeclient
lobby-client-lockclient
:timeoutseconds
defuncall-with-lobby-radio-connection
client
let
connectionnil
transport
lobby-client-transportclient
unwind-protect
progn
setfconnection
open-lobby-transporttransportclient
sb-thread:with-mutex
lobby-client-lockclient
setf
lobby-client-connectionclient
connection
subscribe-lobby-transporttransportconnectionclient
publish-lobby-transporttransportconnectionclient
lobby-client-nameclient
loopuntildo
multiple-value-bind
topicpayload
next-lobby-publicationtransportconnectionclient
whentopic
receive-lobby-publicationclienttopicpayload

Only the worker ever speaks on the connection, including graceful presence deletion and close. STOP merely publishes a local flag.

whenconnection
ignore-errors
publish-lobby-transporttransportconnectionclient""
sb-thread:with-mutex
lobby-client-lockclient
when
eqconnection
lobby-client-connectionclient
setf
lobby-client-connectionclient
nil
ignore-errors
close-lobby-transporttransportconnectionclient
defunrun-lobby-client
client
unwind-protect
loopwithdelay=1/4untildo
publish-lobby-statusclient:connecting:clear-cache-pt
handler-case
progn
setfdelay1/4
error
condition
unless
publish-lobby-statusclient:offline:conditioncondition:clear-cache-pt
setfdelay
min5
*2delay
sb-thread:with-mutex
lobby-client-lockclient
%publish-lobby-statusclient:stopped:clear-cache-pt
when
eq
lobby-client-threadclient
sb-thread:*current-thread*
setf
lobby-client-threadclient
nil
client
defunstart-lobby-client
client

Start client once. A stopped client may be restarted with fresh state.

check-typeclientlobby-client
sb-thread:with-mutex
lobby-client-lockclient
let
thread
lobby-client-threadclient
cond
andthread
sb-thread:thread-alive-pthread
when
lobby-client-stopping-pclient
error"Lobby client ~A is still stopping."
lobby-client-nameclient
t
setf
lobby-client-threadclient
nil
lobby-client-stopping-pclient
nil
%publish-lobby-statusclient:starting:clear-cache-pt
setf
lobby-client-threadclient
sb-thread:make-thread
lambda
:name
formatnil"~A lobby radio"
lobby-client-id-prefixclient
client
defunstop-lobby-client
client&key
timeout3/4

Request cooperative stop and wait at most timeout seconds.

No transport operation occurs on the caller. Repeated stops are idempotent.

whenclient
let
threadnil
sb-thread:with-mutex
lobby-client-lockclient
setfthread
lobby-client-threadclient
whenthread
setf
lobby-client-stopping-pclient
t
%publish-lobby-statusclient:stopping:clear-cache-pt
sb-thread:condition-broadcast
lobby-client-wakeclient
when
andthread
not
eqthreadsb-thread:*current-thread*
multiple-value-bind
valuestate
sb-thread:join-threadthread:timeouttimeout:default:timeout
declare
ignorevalue
when
eqstate:timeout
error'lobby-worker-stop-timeout:clientclient:secondstimeout

run-lobby-client normally clears this itself. Clearing a dead thread here also recovers cleanly if an implementation returns before its unwind cleanup becomes visible.

sb-thread:with-mutex
lobby-client-lockclient
when
and
lobby-client-threadclient
not
sb-thread:thread-alive-p
lobby-client-threadclient
setf
lobby-client-threadclient
nil
client