Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a4959f2
Always specify the id as a keyword argument.
Feb 3, 2026
257484f
Honor a client request to omit the ACK response.
Feb 3, 2026
c7ad4fd
Honor a client request to omit the ACK response.
Feb 3, 2026
f3a5811
Allow arbitrary additional arguments in a payload. Use that capabilit…
Feb 3, 2026
dec3a21
Disable the high water mark for request/response; it was coming up wi…
Feb 3, 2026
9039a28
Put the client background thread back the way it was, the changes there
Feb 3, 2026
99ca2a0
Change the new 'ack' argument to set() to be 'response' instead.
Feb 4, 2026
deb8d75
Trade out the 'ack' field of the payload for 'silent'.
Feb 4, 2026
1b88742
Trade out the 'ack' field of the payload for 'silent'.
Feb 4, 2026
dcdec27
Trade out the response argument in set() for silent.
Feb 4, 2026
8d1a21d
Trade out the 'ack' field of the payload for 'silent'.
Feb 4, 2026
ca3b193
Added a comment adjacent to the call to req_handler() indicating that…
Feb 4, 2026
eecd838
Docstring tweak.
Feb 4, 2026
4bc95ff
Added a comment about potential out-of-order processing for high freq…
Feb 4, 2026
65b990b
Trade out the 'silent' argument for 'reply' instead.
Feb 4, 2026
22bd3a3
I was trying to realign that background thread with the original
Feb 6, 2026
3426b8e
Use a property for the 'reply' attribute on a Payload.
Feb 10, 2026
b278fa6
Mirror the Payload.reply attribute as Message.reply to simplify excep…
Feb 10, 2026
374da6c
Payload.reply is now a property (mirrored to Message.reply) to simplify
Feb 10, 2026
99f2e84
Merge branch 'main' into no_ack
klanclos Feb 10, 2026
51e0677
Merge remote-tracking branch 'origin/main' into no_ack.
Apr 3, 2026
9539e02
Sketching what it might look like to use Message-centric flags for
Apr 22, 2026
a70f656
Merge remote-tracking branch 'origin/main' into no_ack
Apr 22, 2026
493d440
A couple extra touches to handle the optional 'flags' argument.
Apr 23, 2026
45a6ce4
Add a Message.ack property to return the 'no ack' status the same
Apr 23, 2026
93f55f5
Remove 'reply' awareness from the Payload.
Apr 23, 2026
8c67d04
Request needs to pass on the flags to the parent init.
Apr 23, 2026
dec9ab1
Check request.ack instead of request.reply on whether to ACK.
Apr 23, 2026
457e622
Use message envelope flags instead of the payload to indicate
Apr 23, 2026
43c887d
Check request.ack and request.reply separately.
Apr 23, 2026
e8f0c16
Define a combined flag to request no ACK or REP response.
Apr 23, 2026
13bd055
Trade out 'NO_REPLY' for 'NO_REP'.
Apr 23, 2026
153c1bf
Rearrange the ordering of the parts to put 'flags' later in the seque…
Apr 23, 2026
dab7acf
Add documentation of the 'flags' message component.
Apr 23, 2026
f52af24
Allow transmission of the flags as the empty byte string, equivalent to
Apr 23, 2026
fe7361a
Touching up the request/response section.
Apr 23, 2026
f77eca4
Use request.ack to decide whether to issue an ACK response.
Apr 23, 2026
c8230fb
Execute the request if no reply is requested, just don't reply.
Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 72 additions & 45 deletions doc/protocol.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,40 @@ implementation in ZeroMQ, which enforces a strict one request, one response
pattern; instead, we use DEALER/ROUTER, which allows any amount of messages
in any order, in any direction.

Upon receipt of a request the daemon will immediately issue an ACK response.
The absence of a quick response indicates that the daemon is not available,
and the client should immediately raise an error. After the client receives
the initial ACK it should then look for the full response. There will be no
further messages associated with that id number after the full response is
received. A daemon may choose to forego the ACK response, but should only
do so in circumstances where processing a request requires zero additional
processing time.

All requests are handled
fully asynchronously; a client could send a thousand requests in quick
succession, but the responses will not be serialized, and the response order
is not guaranteed. Synchronous behavior, if desired, is implemented by client
code and not in the protocol itself.
Here is an example of what the full exchange on the client side might look
like, in this case handling the exchange as a synchronous request::

self.socket = zmq_context.socket(zmq.DEALER)
self.socket.setsockopt(zmq.LINGER, 0)
self.socket.identity = identity.encode()
self.socket.connect(daemon)

self.socket.send_multipart(request)
result = self.socket.poll(100) # milliseconds
if result == 0:
raise TimeoutError('no response received in 100 ms')

ack = self.socket.recv_multipart()
response = self.socket.recv_multipart()

The request/response interaction between the client and daemon is a multipart
message, where each part is required and has specific meaning. The reference
implementation provides a :class:`mktl.protocol.message.Message` class to
minimize the amount of code that has to be aware about the on-the-wire message
structure. For both ends of the request/response exchange, the message parts
are:
message, where each component of the message has a specific meaning.
The message parts are identical for both ends of the request/response
exchange.

.. list-table::

Expand All @@ -67,6 +95,7 @@ are:
* - **version**
- A single ASCII character indicating the mKTL protocol version number.
The initial release of the mKTL protocol uses the version character 'a'.
The version identifier is always present.

* - **identifier**
- A unique identifier for the request. The format of this identifier is
Expand All @@ -76,87 +105,85 @@ are:
to the original request. Note that this identifier does not necessarily
have significance on the daemon side, daemons will use their own internal
scheme to uniquely identify requests, but the response will always include
this original identifier.
this original identifier. The request identifier is always present.

* - **type**
- The message type. This is a short string of characters that identifies
what type of request, or response, this message represents. It is one
of the values described in the :ref:`message_types` section below.
of the values described in the :ref:`message_types` section below. The
message type is always present.

* - **target**
- The target for this request/response, if any. Not all requests have a
target; responses don't need to specify it, since it is the identification
number that ties a response to its request. If a target is specified it
target; responses don't need to specify it, since the identifier field
associates a response with a request. If a target is specified it
is a store or a key, depending on the request; this field will be an empty
byte sequence if the target is not specified.

* - **flags**
- A big-endian integer representing boolean flags that modify how this
message is handled. The default value is an integer zero; if this field
is transmitted as an empty byte sequence it must be interpreted as the
integer zero. Each bit in the integer has a specific meaning:

.. list-table::

* - *Bit*
- *Name*
- *Meaning*

* - 0b0001
- NO_ACK
- Suppress the ACK response to this request.

* - 0b0010
- NO_REP
- Suppress the REP response to this request.

* - **payload**
- The message payload. This is the JSON representation of any additional
data required as part of this exchange; if setting a new value, it would
contain the value; if it is a response containing additional information
it would go here. This field will be an empty byte sequence if no
additional information is required. See the :ref:`message_payload` section
for a more complete description of the payload contents.
it would go here. See the :ref:`message_payload` section
for a more complete description of the payload contents. This field will
be an empty byte sequence if there is no payload.

* - **bulk**
- A bulk byte sequence, typically a component of the payload. This is to
allow the transmission of information like image data, where the bulk
bytes represent the image buffer, and the JSON payload describes how
to interpret the buffer. This field will be omitted entirely if there
is no bulk component.

Upon receipt of a request the daemon will immediately issue an ACK response.
The absence of a quick response indicates that the daemon is not available,
and the client should immediately raise an error. After the client receives
the initial ACK it should then look for the full response. There will be no
further messages associated with that id number after the full response is
received. A daemon may choose to forego the ACK response, but should only
do so in circumstances where processing a request requires zero additional
processing time.

All requests are handled
fully asynchronously; a client could send a thousand requests in quick
succession, but the responses will not be serialized, and the response order
is not guaranteed. Synchronous behavior, if desired, is implemented by client
code and not in the protocol itself.

Here is an example of what the full exchange on the client side might look
like, in this case handling the exchange as a synchronous request::

self.socket = zmq_context.socket(zmq.DEALER)
self.socket.setsockopt(zmq.LINGER, 0)
self.socket.identity = identity.encode()
self.socket.connect(daemon)

self.socket.send_multipart(request)
result = self.socket.poll(100) # milliseconds
if result == 0:
raise zmq.ZMQError('no response received in 100 ms')

ack = self.socket.recv_multipart()
response = self.socket.recv_multipart()
is no bulk component, which allows a recipient to distinguish between
an empty byte sequence and the complete absence of data.

Here is a representation of what the on-the-wire messages might look like
for the simple exchange outlined above::
for a simple GET request::

b'a'
b'00000023'
b'GET'
b'kpfguide.LASTFILENAME'
b'\x00'
b''

b'a'
b'00000023'
b'ACK'
b''
b''
b''

b'a'
b'00000023'
b'REP'
b''
b''
b'{"value": /sdata1701/kpf1/2025-06-23/image_672.fits', "time": 234.23}'

The reference implementation provides a :class:`mktl.protocol.message.Message`
class to minimize the amount of code that has to be aware about the on-the-wire
message structure.


.. _message_types:

Expand Down
6 changes: 4 additions & 2 deletions sbin/mkbrokerd
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@ class RequestServer(mktl.protocol.request.Server):
will be generated.
"""

self.req_ack(request)
if request.ack:
self.req_ack(request)

type = request.type
target = request.target
Expand All @@ -415,7 +416,8 @@ class RequestServer(mktl.protocol.request.Server):
else:
raise ValueError('invalid request type: ' + type)

return payload
if request.reply:
return payload


def req_hash(self, request):
Expand Down
12 changes: 8 additions & 4 deletions src/mktl/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,12 @@ def req_config(self, request):


def req_handler(self, request):
""" Inspect the incoming request type and decide how a response
will be generated.
""" Inspect the incoming request type and call an appropriate
method to handle that specific request.
"""

self.req_ack(request)
if request.ack:
self.req_ack(request)

type = request.type
target = request.target
Expand All @@ -511,7 +512,10 @@ def req_handler(self, request):
else:
raise ValueError('unhandled request type: ' + type)

return response
if request.reply:
return response
else:
return None


def req_get(self, request):
Expand Down
27 changes: 19 additions & 8 deletions src/mktl/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,15 +661,18 @@ def req_set(self, request):
return payload


def set(self, new_value, wait=True, formatted=False, quantity=False):
def set(self, new_value, wait=True, reply=True, formatted=False, quantity=False):
""" Set a new value. Set *wait* to True to block until the request
completes; this is the default behavior. If *wait* is set to False,
the caller will be returned a :class:`mktl.protocol.message.Request`
instance, which has a :func:`mktl.protocol.message.Request.wait`
method that can optionally be invoked to block until completion of
the request; the wait will return immediately once the request is
satisfied. There is no return value for a blocking request; failed
requests will raise exceptions.
satisfied. Set *reply* to False to disable all error handling and
acknowledgements for the request (fire and forget); setting
*reply to False implies *wait* is also False.
There is no return value for a blocking request; failed requests
will raise exceptions.

The optional *formatted* and *quantity* options enable calling
:func:`set` with either the string-formatted representation or
Expand Down Expand Up @@ -699,8 +702,16 @@ def set(self, new_value, wait=True, formatted=False, quantity=False):
raise ValueError('formatted+quantity arguments must be boolean')

payload = self.to_payload(new_value)
payload.add_origin()
message = protocol.message.Request('SET', self.full_key, payload)

if reply:
flags = None
payload.add_origin()
else:
flags = protocol.message.NO_ACK_OR_REP
wait = False
Comment thread
tylertucker202 marked this conversation as resolved.

key = self.full_key
message = protocol.message.Request('SET', key, payload, flags=flags)
self.req.send(message)

if wait == False:
Expand Down Expand Up @@ -841,7 +852,7 @@ def to_format(self, value):
return formatted


def to_payload(self, value=None, timestamp=None):
def to_payload(self, value=None, timestamp=None, **kwargs):
""" Interpret the provided arguments into a
:class:`mktl.protocol.message.Payload` instance; if the *value* is
not specified the current value of this :class:`Item` will be
Expand Down Expand Up @@ -877,11 +888,11 @@ def to_payload(self, value=None, timestamp=None):
bulk = value.tobytes()
except AttributeError:
bulk = None
payload = protocol.message.Payload(value, timestamp)
payload = protocol.message.Payload(value, timestamp, **kwargs)
else:
shape = value.shape
dtype = str(value.dtype)
payload = protocol.message.Payload(None, timestamp, bulk=bulk, shape=shape, dtype=dtype)
payload = protocol.message.Payload(None, timestamp, bulk=bulk, shape=shape, dtype=dtype, **kwargs)

return payload

Expand Down
Loading
Loading