Skip to content

RAII-manage enqueue semaphore permits - #22

Merged
yrivardmulrooney merged 1 commit into
coveooss:mainfrom
the-mikedavis:semaphore-raii
Jul 27, 2026
Merged

RAII-manage enqueue semaphore permits#22
yrivardmulrooney merged 1 commit into
coveooss:mainfrom
the-mikedavis:semaphore-raii

Conversation

@the-mikedavis

Copy link
Copy Markdown
Contributor

I noticed that the semaphore on the producer side could miscount available credit in a few edge-cases like messages that are too large (semaphore acquired and then an exception is raised) or if there is a confirm timeout (semaphore not released).

I'm more familiar with Rust so naturally I want to use Drop-glue to fix this 😄. The idea is to have the semaphore hand out Permits. On drop/destroy of an AccumulatedMessage, the Permit releases its hold on the semaphore.

This is a departure from the official Java client, the Java client releases its semaphore in calling code. I think it's more idiomatic to solve this with RAII in C++ since the type can do the book-keeping itself.

This change replaces the explicit acquire/try_acquire_for plus release
API with an RAII "permit" mechanism that enforces correct releases. The
pairing between acquire and release was not really hermetic:

* check_confirm_timeout() removed timed-out messages from
  m_unconfirmed and reported ConfirmTimeout, but didn't release the
  semaphore for them. This could leak capacity and enough timeouts might
  exhaust max_unconfirmed eventually.
* send() acquired the permit before calling accumulator->add(), which
  throws InvalidInputException for a message over max_frame_size.
  That exception isn't one send() catches, so it also leaked a
  permit.
* handle_publish_confirm()/handle_publish_error() released
  unconditionally, even when the publishing id wasn't found in
  m_unconfirmed (a duplicate or late confirm for an already-timed-out
  message). This could over-release.

Semaphore::acquire()/try_acquire_for() now return a Permit that
releases on drop. AccumulatedMessage holds one for as long as it's
outstanding (queued, in flight, or unconfirmed) so the permit's lifetime
is tied directly to the message's lifetime instead of depending on every
code path that can end that lifetime (confirm, error, timeout, oversized
rejection, or shutdown) to remember to release it itself.

For context: the official Java client's equivalent
(unconfirmedMessagesSemaphore) doesn't have this problem because every
path that removes an entry from unconfirmedMessages funnels through the
same release-on-removal logic. RAII enforces this in the type rather
than in the calling code.
@yrivardmulrooney

Copy link
Copy Markdown
Collaborator

Thanks for this PR! The semaphore class was intended to be mostly a simple implementation for a std::counting_semaphore since initially we didn't target c++20 for the library. However, there was some more exotic stuff added to it for the library over time, so I think deviating from the standard API is warranted in this case, as it makes usage less error prone. We're not really attached to the java client either, it was just a good starting point to structure the API of the library, the implementation doesn't have to adhere to its exact semantics (that, and it probably evolved at lot since hareflow was implemented). Good catch on the edge cases!

@yrivardmulrooney
yrivardmulrooney merged commit 3bc2fbd into coveooss:main Jul 27, 2026
5 checks passed
@the-mikedavis
the-mikedavis deleted the semaphore-raii branch July 27, 2026 19:16
@the-mikedavis

Copy link
Copy Markdown
Contributor Author

Thanks! I took a quick look at counting_semaphore and I think it might not buy very much anyways since the semaphore here has extra functionality like destroy(). It would probably only save a handful of lines and I think that code is already straightforward/clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants