Skip to content

feat(search-sync-worker): read HR JetStream domain from HR_JETSTREAM_DOMAIN#466

Merged
GITMateuszCharczuk merged 6 commits into
mainfrom
claude/hr-jetstream-domain-env-xq6szz
Jul 8, 2026
Merged

feat(search-sync-worker): read HR JetStream domain from HR_JETSTREAM_DOMAIN#466
GITMateuszCharczuk merged 6 commits into
mainfrom
claude/hr-jetstream-domain-env-xq6szz

Conversation

@mliu33

@mliu33 mliu33 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What & why

search-sync-worker's spotlight-org collection consumes HR org data from OrgSyncStream(HR_CENTRAL_SITE_ID), which is owned by hr-syncer and can live in a different NATS JetStream domain (a remote cluster in the supercluster). A worker at site A must be able to create a durable consumer on, and fetch from, the HR stream in site B's domain.

Accessing a stream in another JetStream domain requires a JetStream context whose API prefix targets that domain ($JS.<domain>.API.*), fixed at construction via jetstream.NewWithDomain. Today main.go builds a single oteljetstream.New(nc) context used for all collections — local (INBOX / MESSAGES_CANONICAL) and HR — and one context can only serve one domain.

This PR lets the HR domain be supplied via HR_JETSTREAM_DOMAIN, routing only the HR consumer to a domain-scoped context while local collections keep the shared context. Unset behaves exactly as today.

Approach

  • New seam (consumer_source.go). A minimal msgFetcher / msgBatch interface that both consumer flavors satisfy, normalized to raw jetstream.Msg (which handler.Add already consumes):
    • rawConsumerAdapter wraps a raw jetstream.Consumer (its batch already yields raw jetstream.Msg — pass-through).
    • otelConsumerAdapter / otelBatch wrap oteljetstream.Consumer, unwrapping each oteljetstream.Msg to its embedded raw jetstream.Msg.
  • runConsumer refactor. Parameter widened from oteljetstream.Consumer to msgFetcher; loop unwrap add(msg.Msg)add(msg); call site wraps otelConsumerAdapter{cons}. Behavior-preserving.
  • HR-domain wiring (main.go). New HRJetStreamDomain config field (env:"HR_JETSTREAM_DOMAIN" envDefault:""). When set, build hrJS := jetstream.NewWithDomain(nc.NatsConn(), domain) (fail-fast on error). The collection whose stream is OrgSyncStream (streamCfg.Name == hrName) is created against hrJS via rawConsumerAdapter; every other collection uses the shared otel-traced js.

Why the HR context is raw (not otel-wrapped)

oteljetstream (instrumentation-go v0.2.0) exposes only New(conn) — no domain variant — so the domain-scoped context comes from the raw jetstream package. This loses nothing in use: runConsumer already unwraps to raw jetstream.Msg and handler.Add never reads the per-message otel trace context, so the consume-side span for this one stream was already discarded.

Behavior when unset

HR_JETSTREAM_DOMAIN=""hrJS == nil → HR collection takes the shared-js path via otelConsumerAdapter. The only delta vs. today is that local consumers now flow through a pure-unwrap adapter over the same messages — observably identical. HR stream bootstrap is untouched (already skipped; owned by hr-syncer/ops).

Testing

  • Unit tests for both adapters (order-preserving unwrap / pass-through, error propagation) and a config-parse test for HR_JETSTREAM_DOMAIN (default "" and set value).
  • make test SERVICE=search-sync-worker green (race on); make build / make lint clean.
  • Domain routing itself is not unit/integration-tested — a single-node NATS testcontainer can't model JetStream domains; cross-domain consumption is validated in a real multi-cluster environment. The existing TestSearchSyncSpotlightOrg_Integration (empty-domain path) stays green.

Ops

search-sync-worker/deploy/docker-compose.yml gets a commented HR_JETSTREAM_DOMAIN example. Set it in deployments where hr-syncer runs in a different domain; leave unset for single-cluster.

No client-API doc impact (internal consumer wiring, not a chat.user. handler).

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional support for consuming HR sync data from a remote JetStream domain when configured.
    • Documented the new environment variable in local deployment setup.
  • Bug Fixes

    • Improved consumer handling so HR data can be fetched consistently across local and cross-domain deployments.
    • Kept existing behavior unchanged when the new setting is not provided.
  • Tests

    • Added coverage for the new configuration option and message-consumption behavior.

claude added 6 commits July 7, 2026 00:50
Read the HR JetStream domain (OrgSyncStream) from HR_JETSTREAM_DOMAIN so a
worker at one site can consume the HR stream in a remote NATS domain, while
local collections keep the shared otel-traced JetStream context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sgh3aGBn7fPpK3kF13xqqc
… from env

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sgh3aGBn7fPpK3kF13xqqc
…DOMAIN

Route the spotlight-org (OrgSyncStream) consumer to a domain-scoped JetStream
context when HR_JETSTREAM_DOMAIN is set, so a worker at one site can consume the
HR stream in a remote NATS domain. Empty domain keeps the shared otel-traced
context, matching current behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sgh3aGBn7fPpK3kF13xqqc
The otelBatch adapter re-channels messages via a goroutine that blocks on an
unbuffered send; document at the drain site that batch.Messages() must be
consumed to completion so a future early-break can't leak it or stall shutdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sgh3aGBn7fPpK3kF13xqqc
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds design/plan documentation and search-sync-worker code enabling the HR (spotlight-org / OrgSyncStream) consumer to fetch from a remote NATS JetStream domain via a new HR_JETSTREAM_DOMAIN env var. It introduces a msgFetcher/msgBatch seam with raw and otel adapters, updates main.go wiring and runConsumer's signature, and adds tests.

Changes

HR JetStream Domain Env Support

Layer / File(s) Summary
Plan and design documentation
docs/superpowers/plans/2026-07-07-hr-jetstream-domain-env.md, docs/superpowers/specs/2026-07-07-hr-jetstream-domain-env-design.md
Adds the implementation plan and design spec describing the seam, config field, wiring changes, test plan, and deployment notes for HR_JETSTREAM_DOMAIN.
Consumer-source seam and adapters
search-sync-worker/consumer_source.go, search-sync-worker/consumer_source_test.go
Introduces msgFetcher/msgBatch interfaces and rawConsumerAdapter/otelConsumerAdapter implementations that normalize fetched messages into raw jetstream.Msg, with unit tests covering ordering and error propagation.
Config field for domain
search-sync-worker/config_test.go, search-sync-worker/main.go
Adds HRJetStreamDomain config field (env HR_JETSTREAM_DOMAIN, default empty) with tests verifying default and set behavior.
main.go domain-scoped consumer wiring
search-sync-worker/main.go, search-sync-worker/deploy/docker-compose.yml
Conditionally initializes a domain-scoped JetStream client, routes HR stream consumer creation through it with rawConsumerAdapter while other collections use otelConsumerAdapter, updates runConsumer's parameter type and message loop, and documents the optional docker-compose env var.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant main as main.go startup
  participant hrJS as Domain-scoped JetStream
  participant js as Shared otel JetStream
  participant fetcher as msgFetcher
  participant runConsumer

  main->>hrJS: NewWithDomain (if HR_JETSTREAM_DOMAIN set)
  alt HR stream and hrJS present
    main->>hrJS: CreateOrUpdateConsumer
    main->>fetcher: wrap as rawConsumerAdapter
  else other collections
    main->>js: CreateOrUpdateConsumer
    main->>fetcher: wrap as otelConsumerAdapter
  end
  main->>runConsumer: start(fetcher)
  runConsumer->>fetcher: Fetch()
  fetcher-->>runConsumer: msgBatch
  runConsumer->>runConsumer: range batch.Messages() and add(msg)
Loading

Possibly related PRs

  • hmchangw/chat#64: Both PRs modify the search-sync-worker JetStream consumption/batch-fetch wiring in main.go, with this PR adding the msgFetcher seam on top of that consumer loop.
  • hmchangw/chat#109: Both PRs modify search-sync-worker consumer wiring in main.go, with this PR refactoring runConsumer to use the new msgFetcher seam.
  • hmchangw/chat#170: Both PRs touch the spotlight-org/HR JetStream consumption path, with this PR adding remote-domain routing for that same consumer.

Suggested labels: ready

Suggested reviewers: Joey0538

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clearly states the main change: search-sync-worker now reads the HR JetStream domain from HR_JETSTREAM_DOMAIN.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/hr-jetstream-domain-env-xq6szz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@search-sync-worker/consumer_source.go`:
- Around line 34-40: Both Fetch methods in rawConsumerAdapter and
otelConsumerAdapter return a bare underlying error, which violates the repo’s
wrapping rule. Update each Fetch implementation to wrap the fetch failure with
contextual text using fmt.Errorf(...: %w, err) before returning, while keeping
the successful path unchanged. Use the Fetch methods on rawConsumerAdapter and
otelConsumerAdapter as the fix points so the error chain remains intact for
existing tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a14d88d-3101-4896-af54-8c7edf190c43

📥 Commits

Reviewing files that changed from the base of the PR and between f3a1da5 and c574be6.

📒 Files selected for processing (7)
  • docs/superpowers/plans/2026-07-07-hr-jetstream-domain-env.md
  • docs/superpowers/specs/2026-07-07-hr-jetstream-domain-env-design.md
  • search-sync-worker/config_test.go
  • search-sync-worker/consumer_source.go
  • search-sync-worker/consumer_source_test.go
  • search-sync-worker/deploy/docker-compose.yml
  • search-sync-worker/main.go

Comment on lines +34 to +40
func (a rawConsumerAdapter) Fetch(n int, opts ...jetstream.FetchOpt) (msgBatch, error) {
b, err := a.c.Fetch(n, opts...)
if err != nil {
return nil, err
}
return b, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bare err returns violate the repo's error-wrapping rule.

Both rawConsumerAdapter.Fetch and otelConsumerAdapter.Fetch return the underlying err unwrapped. The coding guidelines require wrapping errors with context via fmt.Errorf("...: %w", err) and never returning a bare err. Since %w preserves the chain, this won't break the require.ErrorIs assertions in the tests.

As per coding guidelines: "Wrap errors with context using fmt.Errorf("...: %w", err); never return a bare err".

🐛 Proposed fix
 func (a rawConsumerAdapter) Fetch(n int, opts ...jetstream.FetchOpt) (msgBatch, error) {
 	b, err := a.c.Fetch(n, opts...)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("raw consumer fetch: %w", err)
 	}
 	return b, nil
 }
 func (a otelConsumerAdapter) Fetch(n int, opts ...jetstream.FetchOpt) (msgBatch, error) {
 	b, err := a.c.Fetch(n, opts...)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("otel consumer fetch: %w", err)
 	}
 	return otelBatch{b}, nil
 }

Also applies to: 47-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@search-sync-worker/consumer_source.go` around lines 34 - 40, Both Fetch
methods in rawConsumerAdapter and otelConsumerAdapter return a bare underlying
error, which violates the repo’s wrapping rule. Update each Fetch implementation
to wrap the fetch failure with contextual text using fmt.Errorf(...: %w, err)
before returning, while keeping the successful path unchanged. Use the Fetch
methods on rawConsumerAdapter and otelConsumerAdapter as the fix points so the
error chain remains intact for existing tests.

Source: Coding guidelines

@mliu33 mliu33 added the ready label Jul 8, 2026

@GITMateuszCharczuk GITMateuszCharczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Thanks!

@GITMateuszCharczuk GITMateuszCharczuk merged commit 0478d0a into main Jul 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants