Skip to content

Performance test suite on a self-hosted Windows runner - #27

Closed
astubbs wants to merge 3 commits into
dev/maven-publishfrom
dev/self-hosted-runner
Closed

astubbs wants to merge 3 commits into
dev/maven-publishfrom
dev/self-hosted-runner

Conversation

@astubbs

@astubbs astubbs commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

Splits the volume tests into a separate "performance" test group that runs on a self-hosted GitHub Actions runner (Windows + Docker Desktop), so they don't slow down or destabilize the regular CI.

Test changes

  • Tagged with @Tag("performance"):
    • `VeryLargeMessageVolumeTest`
    • `LargeVolumeInMemoryTests`
    • `MultiInstanceHighVolumeTest`
  • Failsafe now reads two new pom properties:
    • `included.groups` (default empty) → `-Dgroups=`
    • `excluded.groups` (default `performance`) → `-DexcludedGroups=`
  • Regular CI builds are unchanged in scope: they exclude performance tests automatically. To override:
    • `-Dincluded.groups=performance` → run only perf
    • `-Dexcluded.groups=` → run everything including perf

Workflow

`.github/workflows/performance.yml`:

  • `runs-on: [self-hosted, windows, performance]`
  • Triggers: `workflow_dispatch` (manual) + weekly cron (Sunday 02:00 UTC)
  • Never runs on PRs — self-hosted runners + untrusted code is a security risk
  • Uploads test reports as artifacts (30 day retention)

Scripts

  • `bin/performance-test.sh` (Linux/macOS local runs)
  • `bin/performance-test.cmd` (Windows runs / what the workflow calls)

Documentation

  • `docs/SELF_HOSTED_RUNNER.md` — one-time runner setup walkthrough for Windows + Docker Desktop + WSL2
  • `AGENTS.md` and the README template mention the perf suite and link to the setup doc
  • Fixed a stale README link to a renamed test file

What you need to do once after merging

  1. Install the runner agent on your Windows desktop following `docs/SELF_HOSTED_RUNNER.md`
  2. Label it with `performance` and `windows` (lowercase)
  3. Trigger the workflow manually from the Actions tab to verify

Stacked on

This PR is stacked on top of #26 (dev/maven-publish), which is itself stacked on #25 and #24.

🤖 Generated with Claude Code

astubbs and others added 3 commits April 21, 2026 10:48
On every push to master, .github/workflows/publish.yml reads the
version from pom.xml and either:
- Publishes a snapshot (version ends in -SNAPSHOT)
- Publishes a release + creates git tag + GitHub release (otherwise)

The pom.xml version is the source of truth - no maven-release-plugin
dance. To cut a release, merge a PR that strips -SNAPSHOT.

Also adds --pinentry-mode loopback to the maven-gpg-plugin so
headless CI signing works without a TTY.

Documents required GitHub secrets in AGENTS.md and the README dev
section:
- MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD
- MAVEN_GPG_PRIVATE_KEY / MAVEN_GPG_PASSPHRASE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a separate performance test suite that runs on dedicated hardware
via a self-hosted GitHub Actions runner, so the volume tests don't
slow down or destabilize regular CI.

Test changes:
- Tag VeryLargeMessageVolumeTest, LargeVolumeInMemoryTests, and
  MultiInstanceHighVolumeTest with @tag("performance")
- Failsafe now reads excluded.groups (defaults to "performance") and
  included.groups (defaults to empty), so the performance group is
  excluded by default. Override with -Dincluded.groups=performance to
  run only perf, or -Dexcluded.groups= to run everything.

Scripts:
- bin/performance-test.sh and bin/performance-test.cmd run the perf
  suite locally (.cmd for Windows since the runner is on Windows)

Workflow:
- .github/workflows/performance.yml targets a self-hosted runner
  labelled [self-hosted, windows, performance]
- Triggers: workflow_dispatch (manual) + weekly schedule
- Never runs on PRs (security: self-hosted + untrusted code = bad)

Documentation:
- docs/SELF_HOSTED_RUNNER.md walks through one-time runner setup on
  Windows with Docker Desktop and the WSL2 backend
- README template and AGENTS.md mention the performance suite and
  link to the runner setup doc
- Fix a stale link in README to the renamed VolumeTests file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a "License headers" section to AGENTS.md explaining how to skip
the Mycila license-maven-plugin check. This avoids two recurring
problems:
- The plugin's git-derived copyright years break inside git worktrees
- It auto-bumps years on every touched file, creating noise in
  git status that distracts from real changes

The bin/build.sh and bin/ci-build.sh scripts already pass the flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@astubbs
astubbs force-pushed the dev/self-hosted-runner branch from 848ddbc to faa3adb Compare April 20, 2026 22:49
@astubbs
astubbs force-pushed the dev/maven-publish branch from 230e5d9 to a66b635 Compare April 20, 2026 22:52
@astubbs
astubbs deleted the branch dev/maven-publish April 20, 2026 22:59
@astubbs astubbs closed this Apr 20, 2026
@astubbs

astubbs commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Closed during the April 2026 rebrand/CI restructure. The branch dev/self-hosted-runner still exists. This self-hosted Windows-runner approach was superseded by GitHub-hosted performance tests (.github/workflows/performance.yml on master). Kept for reference; not planned for merge.

astubbs added a commit that referenced this pull request Sep 2, 2026
…rupt as a wake-up, and end recovery on the failures retrying cannot fix

Recovery lifecycle findings #3, #4, #8, #13, #17, #22, #23 and #27 of the
#410 review, each with a test that was red against
the previous code (run as a mutant of this commit - the old behaviour
reinstated line by line - and green after).

What changed for a user of the PC-built transactional path:

- A replacement producer that was built but failed initTransactions is now
  closed before the retry is scheduled. It was leaked: nothing held a
  reference to it, and each one kept a KafkaProducer network thread - about
  120 per hour at the 30 s backoff cap while a coordinator was unreachable.
  Both catch branches close it (ProducerManagerRecoveryTest:
  aReplacementThatFailsToInitialiseIsClosedBeforeTheRetryIsScheduled,
  aReplacementThatFailsTerminallyIsClosedToo).

- The recovery counter and its log line moved out of the build try. The
  counter is the user's MeterRegistry, which has thrown from inside PC
  before (docs/solutions/runtime-errors/a-throwing-meter-registry-kills-the-
  poll-thread-and-strands-close.md); a throw there turned a published,
  in-use replacement into a DEFERRED outcome that scheduled a second
  rebuild against it (aThrowingMeterRegistryDoesNotTurnACompleted
  ReplacementIntoADeferredOne).

- An interrupt while recovery waits for the producer write lock no longer
  closes the instance. notifySomethingToDo interrupts the control thread
  whenever the write lock is not HELD, and it is not held while WAITING for
  it - which recovery does for as long as a worker holds the produce lock
  through its user function; the rebalance that fenced the producer ends
  with onPartitionsAssigned, which notifies. The InterruptedException
  escaped maybeRecoverProducer's RuntimeException catch and the supervisor
  closed PC with no failure cause. It is now caught, cleared and the pass
  returns; the condition stays recorded and the next pass retries, the same
  shape as the mailbox poll's own catch. maybeRecoverProducer no longer
  declares InterruptedException, so the compiler holds the line
  (ProducerRecoveryTest: aWakeUpInterruptWhileRecoveryWaitsForTheWriteLock
  DoesNotCloseTheInstance - one record only, so the commit path's separate,
  pre-existing exposure to the same interrupt does not take it first).

- An Error from the user's ProducerFactory is terminal. The factory is user
  code and is now wrapped by UserFunctions.carefullyRun like every other
  user function; an Error in the cause chain (a serializer's static
  initialiser failing, say) is deterministic and joins AuthorizationException
  and UnsupportedVersionException as a terminal build failure, so the
  instance closes naming the type, and its parked workers are released by
  the close. Before, the Error escaped every catch on the recovery path:
  the supervisor's catch is Exception, so the instance stayed RUNNING with
  every worker parked on the produce lock for good. maybeRecoverProducer
  also catches Error from anywhere else in the pass, records it as the
  failure reason and transitions to CLOSING before rethrowing, for the same
  release (anErrorFromTheFactoryIsTerminalAndClosesTheInstanceNamingIt).

- A factory contract violation is terminal, not deferred. Returning null,
  returning the producer it already returned, or building a producer that
  does not carry the transactional.id PC handed it now raise
  ProducerFactoryContractException, a dedicated type the terminal predicate
  recognises. Every one is deterministic - a caching factory caches on every
  rebuild, never only the first - so retrying was a WARN-per-attempt loop
  for the life of the instance (aFactoryContractViolationIsTerminalRather
  ThanRetriedForever).

- Recovery is skipped once the instance is CLOSING or CLOSED. A close during
  an outage otherwise waited on a rebuild that blocks up to max.block.ms for
  a producer nobody would use; ProducerManager.close releases the parked
  workers on its own. DRAINING still recovers, because a drain needs a
  producer to finish the work in flight (noReplacementIsAttemptedOnceThe
  InstanceIsClosing, driven directly - the window is the control thread's).

Smaller: the two raw producerWrapper call sites the field javadoc claimed
did not exist (sendOffsetsToTransaction and beginTransaction) go through
producer(), and the javadoc now says exactly which paths read the field
raw (the constructor and initProducer, before any replacement can exist);
restoreWorkDiscardedByAbortedTransaction's count is named at its call site
per the house rule; the pollAndProduceMany guard names producerConfig
first and the Producer instance as deprecated. The terminal failure names
the root cause's type as well as the wrapper's - types only, never
messages, which may carry configuration values (R7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrpH51xNDodaajE4P2nhFg
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