Skip to content

EBP-3103: Add best-practice Python pub/sub reference samples - #8

Merged
aelsammak merged 17 commits into
devfrom
EBP-3103
Sep 28, 2026
Merged

aelsammak merged 17 commits into
devfrom
EBP-3103

Conversation

@aelsammak

@aelsammak aelsammak commented Sep 22, 2026 •

Copy link
Copy Markdown

What is the purpose of this change?

Python equivalent of the JCSMP pub/sub reference samples. The solace-application-development skill gets four baked Python pattern samples plus a shared connection helper under references/python/, so a follow-up story can route Python requests to real, complete, idiomatic code instead of code written from memory. Skill routing, mode files, the description, and eval cases are out of scope here and follow in separate stories.

How is this accomplished?

  1. Added python-solace-connection-config.py: reads a gitignored config.json whose keys are Python API property-name strings (generic pass-through to from_properties()), or falls back to <host:port> <message-vpn> <client-username> [password]. It always supplies the basic-auth password key, which the API requires at build().
  2. Added python-guaranteed-publisher-sample.py, python-guaranteed-subscriber-sample.py, python-direct-publisher-sample.py, and python-direct-subscriber-sample.py, derived from the patterns/ files of the Solace Python samples and structured like the JCSMP samples: setup_solace(), a run loop, and teardown_solace() from main's finally.
  3. Applied the Python developer guide practices per file: reconnection retry strategy with a 3000 ms interval (the API takes milliseconds), service listeners registered before connect(), on_back_pressure_reject paired with a PublisherReadinessListener, a publish receipt listener that checks the receipt exception and correlates by user_context, a PublishFailureListener on the direct publisher, CREATE_ON_START plus with_subscriptions() so the provisioned queue attracts messages, explicit client ack after processing, and graceful terminate() then disconnect() on SIGINT and SIGTERM.
  4. Applied the C API Best Practices that carry over to the Python wrapper: do-not-block notes on every callback, an HA reconnect comment block, tolerant payload decoding so an unexpectedly formatted message is still acknowledged, and a commented time-to-live option on the guaranteed publisher.
  5. Comment-only edits to five JCSMP samples: removed the legacy product name and the comparisons to upstream samples.

Also disabled trigger evals from running in CI since we dont have a repo secret for Anthropic API Key.

Anything reviewers should focus on/be aware of?

  • Verification is local only. A no-broker smoke test (21 cases: helper file, argument, and fail-fast paths; service, publisher, and receiver build against localhost; every listener class instantiated; a non-UTF-8 payload still acknowledged) and live publisher to subscriber round trips per pair against a local software broker container. Both pairs delivered all 26 rotating payload letters with no warnings, the queue was empty afterwards, and publish receipts arrived correlated by message id. The drivers are not part of this PR; they belong to the Python verify story.
  • No skill description, routing, or eval content changed, so the trigger and output evals were not run. CI compile-check still covers the JCSMP comment edits.

- Add python-solace-connection-config.py plus guaranteed and direct publisher and subscriber samples under references/python, derived from the Solace Python samples and elevated to the Python developer guide and C API best practices
- Register service listeners before connect(), use a 3000 ms reconnect interval, pair reject back pressure with a readiness listener, check publish receipts, subscribe the CREATE_ON_START queue to the topic, client-ack after processing, and shut down gracefully on SIGINT and SIGTERM
- Read config.json or command-line arguments through the shared helper, mirroring jcsmp-solace-connection-config.java, with no version pin
- Reword JCSMP sample comments to drop the legacy product name and the comparisons to upstream samples
- Verify locally against solace-pubsubplus resolved from PyPI with a no-broker smoke test and live round trips per pair
- Drop `from __future__ import annotations` and its explanatory comment from the five Python reference samples
- The built-in generics in the signatures run natively on every Python release that Solace supports, so the shim added nothing
- Match the upstream SolaceSamples/solace-samples-python style, which carries no future import
- Verified: all five modules import with solace-pubsubplus installed
- Replace the ten-line HA and replication comment with a five-line HA-only comment that mirrors the JCSMP sample
- List the C API Best Practices values as Python API calls: connection and reconnection retry strategies plus the CONNECTION_RETRIES_PER_HOST transport property
- Drop the replication guidance, the forever_retry remark, and the comma-separated host list
- Shorten the back pressure comment above PUBLISH_BUFFER_CAPACITY to one line in both publisher samples
- Shorten the module docstring of python-solace-connection-config.py while keeping every contract point (key names, pass-through, override order, gitignore rule, empty-password handling)
- Move the AI-assisted code header block to the end of the docstring to match the other Python samples
- Add a job-level `if: false` to trigger-evals: no policy permits an API key on this public repo, so the job could only self-skip and asserted nothing
- Keep the job definition so it can be re-enabled by removing the `if` once a key is allowed; trigger evals continue to run locally
- Add __pycache__/ to .gitignore so byte-compiling the Python reference samples leaves no untracked files
- Register the SIGINT/SIGTERM handlers before setup_solace(), so a SIGTERM during the blocking connect() still reaches teardown instead of killing the process
- Return exit code 1 from every failure exit (failed queue bind, service interruption, receiver termination, publish() failure); main() now returns the code and __main__ passes it to sys.exit()
- Correct the guaranteed subscriber's terminate() comments: the receiver turns TERMINATED once its buffer is empty, so the ack() of the message still in the handler can fail and the message is redelivered
- Verified against a live broker: both round trips exit 0, and the bind failure, a service interruption (reconnect exhausted), and SIGTERM during connect each run teardown and exit 1
- Describe why the samples bound terminate(): a predictable shutdown time, for example within a process supervisor's stop timeout, so the disconnect still runs
- State the API default (600000 ms) as a fact and invite adjusting the value to suit the application
- Name MessageDestinationDoesNotExistError for the no-subscription-match NACK, matching the library's subcode mapping
- Drop "a broker rejection" from the direct publisher failure listener comment; broker events never reach that listener
- Split terminate() handling into IncompleteMessageDeliveryError and other client errors, and exit non-zero on both
- Update the exit_code comments to match, and drop the inaccurate "failed bind" case in the guaranteed subscriber
- Describe the disabled trigger-evals CI job in the evals README and point to local runs
- Include *.py in the link check so the Python sample header URLs are swept
- Use the claude CLI aliases (sonnet, opus) in the run examples so they always select the latest model in each family
- Describe the runner defaults, the CI matrix, and the judge by model family instead of pinned IDs
- Remove the stale claude-opus-5 reference
@aelsammak
aelsammak marked this pull request as ready for review September 23, 2026 17:06
@mtite-sol

Copy link
Copy Markdown

Just a general question - do we have any automated regression that can detect when your code examples become stale with product? For example, a function becomes deprecated, args change, name change, etc is there something that alerts us that samples need to be re-worked?

Also this is more of an edge case but what about when a customer uses the skill gets something half done but then the api updates and breaks existing code - is there anything that helps update their code automatically?

@aelsammak

aelsammak commented Sep 23, 2026 •

Copy link
Copy Markdown
Author

@mtite-sol

Just a general question - do we have any automated regression that can detect when your code examples become stale with product? For example, a function becomes deprecated, args change, name change, etc is there something that alerts us that samples need to be re-worked?

Right now we have a compile-check that runs in CI which compiles every JCSMP sample against the latest sol-jcsmp release to ensure that nothing broke in the latest release. There is also a story to expand the compile-check to cover Python as well.

However, the compile-check does not fail on deprecation warnings since a deprecated method/function still compiles.

Also this is more of an edge case but what about when a customer uses the skill gets something half done but then the api updates and breaks existing code - is there anything that helps update their code automatically?

There is nothing in place to update their code automatically, I also am not sure what mechanism would do that. I can say though that when a user begins developing with the skill for JCSMP, the skill will pull the latest JCSMP off of maven central and then pin that version in the pom file. So if the next release after that was a major version bump, I would hope that Claude would be smart enough to flag the version to be the problem if there were issues.

@mtite-sol mtite-sol left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Added some non-blocking questions / comments but otherwise LGTM!

@mtite-sol

Copy link
Copy Markdown

@mtite-sol

Just a general question - do we have any automated regression that can detect when your code examples become stale with product? For example, a function becomes deprecated, args change, name change, etc is there something that alerts us that samples need to be re-worked?

Right now we have a compile-check that runs in CI which compiles every JCSMP sample against the latest sol-jcsmp release to ensure that nothing broke in the latest release. There is also a story to expand the compile-check to cover Python as well.

However, the compile-check does not fail on deprecation warnings since a deprecated method/function still compiles.

Also this is more of an edge case but what about when a customer uses the skill gets something half done but then the api updates and breaks existing code - is there anything that helps update their code automatically?

There is nothing in place to update their code automatically, I also am not sure what mechanism would do that. I can say though that when a user begins developing with the skill for JCSMP, the skill will pull the latest JCSMP off of maven central and then pin that version in the pom file. So if the next release after that was a major version bump, I would hope that Claude would be smart enough to flag the version to be the problem if there were issues.

Good to know we already have a compile-check as part of CI that should help to solve most cases! Could be worth augmenting it to detect deprecation warnings though so we don't lag on giving customers best practices.

For the updating code automatically comment might just be something to test later to see if Claude's able to resolve conflicts like that automatically. It might be a non-issue.

- Replace the Java-style app classes with module-level functions (main, setup_solace, run_publish_loop or await_messages, teardown_solace) that share one dataclass state
- Keep the listener classes, since the Python API requires listener subclasses; they now take the shared state
- Replace the SolaceConnectionConfig class with one load_service_properties() function that returns the from_properties() dict
- Use X | None type hints for the Python 3.10+ floor
- Publishers: on_back_pressure_wait(50) replaces on_back_pressure_reject(1000), so the readiness listener and the PublisherOverflowError retry are removed
- Direct subscriber: bound the receive buffer with on_back_pressure_drop_latest(50)
- Verified with the smoke test, both live round trips, and a frozen-proxy test of a blocked publish() (returns on drain, or raises after the reconnect budget)
- setup_solace() now builds the service and the publisher or receiver
  before any connection exists, and returns a fully populated state, so
  no state object is created empty and filled in later (no | None fields)
- New connect_solace() connects the service and starts the endpoint;
  main() calls setup_solace() before its try and runs teardown in finally
- Remove the reconnect pause from both publishers: a guaranteed publish
  waits in the buffer during a reconnect, and the direct publisher now
  documents that messages published during a reconnect are lost and
  reported through the publish failure listener, pointing to guaranteed
  messaging for lossless delivery
- Raise the back pressure buffer from 50 to 1000, the value the Python
  developer guide uses in its examples (the API has no default capacity),
  and note that it counts messages and is tunable to deployment memory
… ones

- Direct publisher passes the payload straight to publish() with no
  OutboundMessageBuilder, and shows additional_message_properties for
  per-message headers; a comment notes the builder suits "templated"
  messages with many fixed headers
- Guaranteed publisher uses the builder as a template: the time-to-live
  (new MESSAGE_TTL_MS, 60 s) is set once, since build() copies the
  template into each message, and the per-message application message
  id goes through build()'s additional_message_properties so it never
  changes the template
- Remove APPROX_MSG_RATE_PER_SEC and the delay after each send from the
  Python and JCSMP direct and guaranteed publisher samples, so the loop
  publishes as fast as send() or publish() returns, the way an
  application publishes each message as soon as it has one
- Update the demo-harness rule in implement-mode.md: the continuous
  publish loop replaces the pacing Thread.sleep in the list, and
  generated applications never add a delay or rate limit between sends
  that the application does not ask for
- Output evals not run for the implement-mode.md change
- Set the reconnection retry strategy before from_properties() in all
  four samples: the builder merges calls in order and a later call wins,
  so the code's strategy is now a default that reconnection keys in the
  properties (for example config.json) override; update the connection
  config helper docstring to match
- Load the Solace properties in main() and pass them to setup_solace(),
  so converting the application's input (config.json or argv) stays out
  of the Solace setup
- Log a buffered-message loss at direct publisher shutdown as a warning
  and keep exit code 0, since direct (at-most-once) messaging allows it


def run_publish_loop(state: PublisherState) -> None:
threading.Thread(target=print_stats, args=(state,), name="stats", daemon=True).start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Might be a nick pick but, shouldn't this thread get joined? the daemon thread mean its only cleaned up on system exit.

- Call receive_async() before receiver.start() in both subscriber samples, so the handler is in place when delivery begins and messages do not wait in the API buffer
- Keep receive_async() after connect(), since _can_receive_message() requires a connected service
- Address PR #8 review comment on the guaranteed subscriber sample
- Rename the four Python samples to python-<delivery>-<role>-async-sample.py
- Update the generated project file names to match, for example guaranteed_subscriber_async.py
- Use the async/sync terms from the Python developer guide, so later sync samples (publish_await_acknowledgement, receive_message) fit the scheme
- Add guidance to the guaranteed publisher on how to choose the message TTL: at least the longest consumer recovery time (restart plus reconnect), or a multiple of it

@cjwmorgan-sol cjwmorgan-sol left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@aelsammak
aelsammak merged commit ca87f35 into dev Sep 28, 2026
4 checks passed
@aelsammak
aelsammak deleted the EBP-3103 branch September 28, 2026 13:51
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.

3 participants