Skip to content

Epic: per-project event sequencing, open-at-head queries, reliable archiving - #35

Merged
johardi merged 4 commits into
mainfrom
epic/303-sse
Jul 27, 2026
Merged

Epic: per-project event sequencing, open-at-head queries, reliable archiving#35
johardi merged 4 commits into
mainfrom
epic/303-sse

Conversation

@johardi

@johardi johardi commented Jul 27, 2026

Copy link
Copy Markdown
Member

This service's share of protegeproject/webprotege-gwt-ui#303 — four atomic commits, landed via #32, #33, #34.

What this adds: the events history query now requires view permission (it previously answered anyone); events get per-project, atomic, gapless sequence numbers assigned at persist time (replacing a global JVM-locked counter) with an idempotent seed migration; a post-persistence sequenced event feeds the gateway's push so anything pushed is already fetchable; the query gains a latest-position anchor mode (project open no longer replays the whole archive) and a bounded, indexed result window; and a failed archive write is no longer acknowledged — it retries with bounded backoff and dedupes on redelivery by event id.

Verified: 21/21 module tests (Testcontainers Mongo + Rabbit) and the 85-test full-stack suite on the epic images. See the gateway epic PR for the before/after performance table.

johardi added 4 commits July 24, 2026 20:36
The handler answering the project-events pull query implemented the plain
CommandHandler type, so any logged-in user could fetch the full change
history of any project just by supplying its id. The WebSocket subscribe
path already required the ViewProject capability; the pull path now goes
through AuthorizedCommandHandler with the same capability on the same
project resource, so the two paths cannot disagree.

A null project id is rejected as a bad request instead of blowing up
inside the authorization wrapper, and a contract test pins the resource
and capability so a refactor cannot silently drop the check.

The webprotege-authorization dependency is pinned to 2.0.1 directly:
Maven mediation previously resolved 0.9.3 (via backend-api), which lacks
the Capability classes the authorized handler needs. The unused
BuiltInAction enum was built on 0.9.3's removed ActionId API and is
deleted with the bump.

Fixes the service half of protegeproject/webprotege-gwt-ui#295
(part of protegeproject/webprotege-gwt-ui#303).
The archive numbered events from a single global counter incremented with
a read-then-write under a JVM-local lock: not per project, and not safe
with more than one service instance. Each project now has its own counter
document advanced with a single findAndModify $inc, which MongoDB runs
atomically server-side, so ordinals stay dense and gapless per project
under concurrency. A startup migration seeds each project's counter to
the maximum ordinal already archived, using setOnInsert so it is
idempotent and never rewinds a counter that has advanced — newly minted
numbers always stay above any bookmark a connected client still holds.

After the archive write, the service now re-publishes the change bundle
as a SequencedPackagedProjectChangeEvent carrying the assigned ordinal.
The gateway will push that instead of the raw event, which is what lets
it stamp truthful event tags — and because publication happens strictly
after persistence, anything a client hears about is already fetchable
through the pull path. Delivery hardening of this publish is #299's
scope; the failure is not silently swallowed here.

A compound index on (projectId, timeStamp) backs the pull query, and a
read-only current-sequence accessor is exposed for the project-open
anchor (#301).

Fixes the service half of protegeproject/webprotege-gwt-ui#296
(part of protegeproject/webprotege-gwt-ui#303).
…s window

The events query gains a latestOnly mode: instead of returning archived
events it answers with an empty window positioned at the project's
current head, read from the per-project sequence counter. A client
opening a project uses it to start listening for live changes without
downloading the past.

The normal query is no longer unbounded: results come back ordered by
sequence with a configurable cap (default 500), and the response's end
tag marks the end of the returned window, so a far-behind client pages
forward naturally over successive requests instead of receiving the
whole archive in one response. The compound index from the sequence work
backs the ordered query.

The service half of protegeproject/webprotege-gwt-ui#301
(part of protegeproject/webprotege-gwt-ui#303).
A failed archive write was caught, logged, and acknowledged, so the
event vanished from the catch-up history forever — exactly the store
that reconnecting clients rely on to fill their gaps. The failure now
propagates so the listener container redelivers the message, and a
bounded retry policy with exponential backoff (tunable via
webprotege.events.retry.*) turns transient store hiccups into recovered
writes while stopping a genuinely poison message from hot-looping —
after the attempts are exhausted it is rejected without requeue.
Redelivery cannot duplicate archive rows because the event id is the
Mongo document id, so a replayed save upserts; an integration test pins
that. The listener container is built inside the ipc library, so the
retry policy is attached through a bean post-processor.

A retried attempt that already minted a sequence ordinal leaves a benign
gap in the numbering; ordinals are opaque and monotonic to clients, so
nothing waits on the missing value.

The event-history companion to protegeproject/webprotege-gwt-ui#299
(part of protegeproject/webprotege-gwt-ui#303).
@johardi
johardi marked this pull request as ready for review July 27, 2026 21:59
@johardi
johardi merged commit a4172d2 into main Jul 27, 2026
0 of 2 checks passed
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.

1 participant