A common question when starting with PlusCal is how to implement message queues. While you rarely have to test that your queue works, you often use them to pass information around your system, and often bugs can appear in the communication layer. So it’s useful to know how to simulate them.
Let’s start by making a simple FIFO queue.
variable queue = <<>>
macro push(queue, message)
begin
queue := Append(queue, message);
end macro
macro pop(queue, receiver)
begin
receiver := Head(queue);
queue := Tail(queue);
end macro
The simplest message queue is our above implementation: put the server and client on separate processes and you’re done. In some cases you can even do away with the pushing and pick an already-filled queue as a starting state.
If you want a queue to have a maximum size, you should add that as a checked invariant: QueueSizeInvariant(queue, size) == Len(queue) <= size
. Even if that’s not the focus of your system, it’s a good sanity check to ensure that the system follows your assumptions. As for implementation, you have to determine what happens when you try to push to a full queue? Does it block until there’s space or drop the message?
macro blocking_push(queue, message)
begin
await Len(queue) < max_queue_size \* could be an operator or function
queue := Append(queue, message);
end macro
macro nonblocking_push(queue, message)
begin
if Len(queue) < max_queue_size then
queue := Append(queue, message);
end if;
end macro
If multiple clients are using the same queue, just have each process refer to the same global.
variables queue = <<>>
process client \in some_set
variable message = Null
begin
Poll:
await queue /= <<>>;
message := pop(queue, message);
end process;
Two common cases of messages are “the server accidentally pushes the same message twice” and “the client fails to delete the pulled message”. The former case is doable with queue \o <<message, message>>
. The latter is doable with an either statement:
macro broken_pop(queue, receiver)
begin
receiver := Head(queue);
either
queue := Tail(queue);
or
skip;
end either;
end macro
In a pub/sub system, the server pushes the same message to multiple clients. Instead of a single queue, we can assign one queue to each process.
variable queue = <<>>,
subscribers = 1..3,
sub_queues = [s \in subscribers |-> <<>>];
macro publish(message)
sub_queues := [ s \in subscribers |-> Append(sub_queues[s], message)];
begin
end macro;
process sub \in subscribers
begin
\* sub_queues[self] is appropriate queue
end process;
Sometimes we cannot guarantee that every receiver will receive every message. We can simulate that by pushing to only a subset of the receivers.
macro lossy_publish(message)
begin
with to_receive \in SUBSET DOMAIN sub_queues do
sub_queues := [ s \in subscribers |->
IF s \in to_receive
THEN Append(sub_queues[s], message)
ELSE sub_queues[s]
];
end macro;