Add create_or_update_stream to JetStream - #964
Conversation
The context exposed create_or_update_consumer but no stream equivalent, forcing callers to branch on existence for what is the most common stream operation. Add an idempotent upsert that attempts an update first and falls back to create when the stream does not exist, returning a Stream handle like create_stream.
|
Claude finished @caspervonb's task in 2m 23s —— View job PR Review
SummaryThe implementation is clean and correct. What looks good:
One thing to decide: There's a TOCTOU race under concurrent access — see the inline comment on line 503. Two clients simultaneously hitting a non-existent stream will both get No asyncio issues, no resource leaks, no type annotation problems beyond what already exists in the codebase. |
| try: | ||
| response = await self._api.stream_update(config.name, **config_dict) | ||
| except StreamNotFoundError: | ||
| response = await self._api.stream_create(config.name, **config_dict) |
There was a problem hiding this comment.
Under concurrent access, two clients can both receive StreamNotFoundError from stream_update and then race on stream_create. The second caller would propagate StreamNameAlreadyInUseError rather than succeeding as expected for an "idempotent upsert."
For most single-client setups this is fine, but if truly idempotent behavior under concurrent access is needed, the StreamNameAlreadyInUseError in the fallback stream_create could be caught and resolved with a final stream_update:
try:
response = await self._api.stream_update(config.name, **config_dict)
except StreamNotFoundError:
try:
response = await self._api.stream_create(config.name, **config_dict)
except StreamNameAlreadyInUseError:
response = await self._api.stream_update(config.name, **config_dict)If the current behaviour (propagate StreamNameAlreadyInUseError) is intentional, the docstring's "any other error is propagated" already covers it — just worth a conscious decision either way.
Two concurrent callers can both see the stream as missing and race the fallback create; the loser got StreamNameAlreadyInUseError instead of the idempotent result. Retry with update when the create collides.
…-or-update-stream
|
Review comments addressed, PTAL @claude |
|
Claude finished @caspervonb's task in 1m 33s —— View job Re-review: Race condition addressed correctly ✓
The update-first → create → update-on-collision three-step pattern is correctly implemented and Implementation ( try:
response = await self._api.stream_update(config.name, **config_dict)
except StreamNotFoundError:
try:
response = await self._api.stream_create(config.name, **config_dict)
except StreamNameAlreadyInUseError:
# Lost a create race with a concurrent caller; the stream
# exists now, so updating keeps the operation idempotent.
response = await self._api.stream_update(config.name, **config_dict)This is exactly the pattern suggested in the prior review — clean, correct. Race test ( The test correctly exercises the specific race window:
Both assertions are meaningful: Minor nit (no action needed): The docstring still says "any other error is propagated" — technically This PR looks good to me. |
The JetStream context exposes
create_or_update_consumerbut no stream equivalent, so callers have to branch on existence for the most common stream operation. Adds an idempotent upsert that attempts an update first and falls back to create when the stream does not yet exist, propagating any other error and returning aStreamhandle likecreate_stream.