Skip to content

✨ Kafka server#229

Merged
aleksul merged 1 commit intomainfrom
kafka
Apr 12, 2026
Merged

✨ Kafka server#229
aleksul merged 1 commit intomainfrom
kafka

Conversation

@aleksul
Copy link
Copy Markdown
Owner

@aleksul aleksul commented Apr 10, 2026

Change Summary

Kafka server

Related issue number

Fix #98

Checklist

  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • My PR is ready to review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 10, 2026

Coverage Report

Name Stmts Miss Cover Missing
TOTAL 7094 0 100%

96 files skipped due to complete coverage.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 11, 2026

Deploying repid with  Cloudflare Pages  Cloudflare Pages

Latest commit: aa164a1
Status: ✅  Deploy successful!
Preview URL: https://b766a750.repid.pages.dev
Branch Preview URL: https://kafka.repid.pages.dev

View logs

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Kafka (Redpanda-backed) server implementation to Repid to support publish/subscribe and message acknowledgements via the existing Repid server APIs (Fix #98).

Changes:

  • Introduces KafkaServer, KafkaSubscriber, and KafkaReceivedMessage implementations using aiokafka.
  • Adds Redpanda-based integration fixtures and Kafka-specific integration tests covering ack/nack/reject/reply and subscriber lifecycle.
  • Adds an optional kafka extra (aiokafka) and updates mypy/pre-commit configuration to accommodate the new dependency.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
repid/connections/kafka/message_broker.py New Kafka server implementation (publish/subscribe, connection lifecycle).
repid/connections/kafka/subscriber.py New consumer/subscriber loop with concurrency limiting and manual commits.
repid/connections/kafka/message.py New received-message wrapper implementing ack/nack/reject/reply semantics.
repid/connections/kafka/__init__.py Exposes KafkaServer from the kafka package.
repid/connections/__init__.py Conditionally exports KafkaServer when aiokafka is installed.
tests/integration/conftest.py Adds Redpanda container + kafka_connection fixture; includes Kafka in integration parametrization.
tests/integration/test_kafka_specific.py Adds Kafka-specific integration tests for message actions and subscriber lifecycle.
pyproject.toml Adds kafka optional dependency and mypy override for aiokafka.*.
.pre-commit-config.yaml Adjusts mypy pre-commit entry invocation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@aleksul aleksul force-pushed the kafka branch 2 times, most recently from a768109 to 39402c3 Compare April 11, 2026 18:55
@aleksul aleksul requested a review from Copilot April 11, 2026 18:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +75 to +82
await asyncio.wait_for(event.wait(), timeout=10.0)
await subscriber.close()

assert received_msg is not None
assert received_msg.payload == b"reject_payload"
assert received_msg.content_type == "text/plain"

await received_msg.reject()
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test the subscriber is closed before calling received_msg.reject(). KafkaReceivedMessage.reject() ultimately calls the subscriber’s consumer.commit(...) via the mark-complete callback, but KafkaSubscriber.close() stops the consumer, so committing after close() is likely to fail/flap. Keep the subscriber open until after the message has been acted on (reject/nack/reply), or adjust the Kafka implementation so message actions can still commit after subscriber shutdown.

Suggested change
await asyncio.wait_for(event.wait(), timeout=10.0)
await subscriber.close()
assert received_msg is not None
assert received_msg.payload == b"reject_payload"
assert received_msg.content_type == "text/plain"
await received_msg.reject()
try:
await asyncio.wait_for(event.wait(), timeout=10.0)
assert received_msg is not None
assert received_msg.payload == b"reject_payload"
assert received_msg.content_type == "text/plain"
await received_msg.reject()
finally:
await subscriber.close()

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +137
await subscriber.close()

assert received_msg is not None

assert received_msg.content_type == "application/json"
await received_msg.nack()
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: the subscriber is closed before received_msg.nack() is called. Since nack commits the offset via the consumer, calling it after subscriber.close() (which stops the consumer) is likely to fail/flap. Move await subscriber.close() to after the nack (and any follow-up assertions that require the action), or change the Kafka implementation to allow committing after close.

Suggested change
await subscriber.close()
assert received_msg is not None
assert received_msg.content_type == "application/json"
await received_msg.nack()
assert received_msg is not None
assert received_msg.content_type == "application/json"
await received_msg.nack()
await subscriber.close()

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +197
await asyncio.wait_for(event.wait(), timeout=10.0)
await subscriber.close()

assert received_msg is not None

await received_msg.reply(
payload=b"reply_response",
headers={"is_reply": "true"},
content_type="application/json",
channel=reply_channel_name,
)

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ordering issue: the subscriber is closed before received_msg.reply(...) is invoked, but reply commits via the consumer. With the consumer stopped in subscriber.close(), the commit can fail. Call reply(...) before closing the subscriber (or make Kafka message actions independent of subscriber shutdown).

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +184
for _ in range(100):
try:
kafka_container._container.reload()
port = kafka_container.ports["29092/tcp"][0]
break
except (KeyError, AttributeError):
sleep(0.1)
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an async fixture but it calls time.sleep(...) while waiting for the container port, which blocks the event loop and can slow/flap the entire async test session. Use await asyncio.sleep(...) in this loop (and consider using a monotonic deadline rather than a fixed iteration count).

Suggested change
for _ in range(100):
try:
kafka_container._container.reload()
port = kafka_container.ports["29092/tcp"][0]
break
except (KeyError, AttributeError):
sleep(0.1)
loop = asyncio.get_running_loop()
deadline = loop.time() + 10
while loop.time() < deadline:
try:
kafka_container._container.reload()
port = kafka_container.ports["29092/tcp"][0]
break
except (KeyError, AttributeError):
await asyncio.sleep(0.1)

Copilot uses AI. Check for mistakes.
"supports_acknowledgments": True,
"supports_persistence": True,
"supports_reply": True,
"supports_lightweight_pause": False,
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supports_lightweight_pause is set to False, but KafkaSubscriber.pause()/resume() uses Kafka’s native consumer.pause()/resume() and does not require disconnecting/re-subscribing. Runner uses this capability flag for backpressure (repid/_runner.py:291+), so marking it false will prevent pausing even though it appears supported. Consider setting supports_lightweight_pause to True (and updating the corresponding integration assertion).

Suggested change
"supports_lightweight_pause": False,
"supports_lightweight_pause": True,

Copilot uses AI. Check for mistakes.
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
except asyncio.CancelledError:
pass
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_consume_loop() only suppresses CancelledError. If consumer.getmany() (or iteration/processing) raises any other exception (e.g. connection issues), the task will exit and the consumer will never be stopped, potentially leaking resources and leaving the subscriber silently inactive. Handle unexpected exceptions (log + retry with backoff, or transition to a closed state and stop the consumer in a finally).

Suggested change
pass
self._closed = True
raise
except Exception as exc:
self._closed = True
logger.exception("subscriber.consume_loop.error", exc_info=exc)
finally:
self._closed = True
with contextlib.suppress(Exception):
await self._consumer.stop()

Copilot uses AI. Check for mistakes.
@aleksul aleksul merged commit 166411d into main Apr 12, 2026
13 checks passed
@aleksul aleksul deleted the kafka branch April 12, 2026 01:22
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.

[FEATURE] Kafka integration

2 participants