Skip to content

fix: receive loop could break if re-auth was called more than once at…#206

Merged
highesttt merged 1 commit into
mainfrom
highest/plat-37819
Jul 6, 2026
Merged

fix: receive loop could break if re-auth was called more than once at…#206
highesttt merged 1 commit into
mainfrom
highest/plat-37819

Conversation

@highesttt

Copy link
Copy Markdown
Collaborator

… a time

@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

PLAT-37819

@indent-zero

indent-zero Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes a bug in the LINE SSE receive loop where the line.Client was constructed once and reused across reconnects. Because line.Client bakes the access token at construction time, any token refresh happening outside the 401/403 SSE recovery path (e.g., a concurrent runTokenRecovery from another API call) left the receive loop reconnecting with a stale token. pollLoop now rebuilds the client at the start of every iteration so the freshest token is always used.

  • Rebuild client = lc.newClient() at the top of each pollLoop iteration and drop the now-redundant refresh inside the 401/403 branch (pkg/connector/sync.go).
  • Introduce package-level test seams getLastOpRevisionWithClient, listenSSEWithClient, and sseReconnectDelay, mirroring the existing newLineAPIClient pattern.
  • Add TestPollLoopRebuildsSSEClientAfterReconnect which flips the token between SSE attempts and asserts the next call uses the new token.
  • Remove leftover fmt.Printf DEBUG statements from Runner.ChannelCreate and add a comment noting the pure-Go channel is a best-effort cache while the WASM channel is authoritative.
  • Add TestChannelCreateWithWASMOnlyKeyDoesNotWriteStdout (uses KeyGenerate, which hits the "no goKey" branch) to lock in the no-stdout-noise behavior.

Issues

1 potential issue found:

  • listenSSEWithClient puts ctx as its second parameter instead of first, deviating from Go's ctx-first convention (the wrapped client.ListenSSE already has ctx first). → Autofix

CI Checks

All CI checks passed on f1beb43.


⚡ Autofix All Issues

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved connection recovery so SSE polling reconnects more reliably after interruptions.
    • Reduced unwanted console output during channel creation.
  • Tests

    • Added coverage for reconnect behavior and for silent channel creation in normal use.

Walkthrough

This PR refactors the LINE SSE polling loop in pkg/connector/sync.go to use overridable function variables for revision fetching and SSE listening, with a configurable reconnect delay, plus a corresponding test. Separately, it silences debug logging in pkg/runner.go's pure-Go channel caching and adds a stdout-verification test.

Changes

SSE Reconnect Refactor

Layer / File(s) Summary
SSE reconnect helper wiring and delay
pkg/connector/sync.go
Introduces package-level function variables (getLastOpRevisionWithClient, listenSSEWithClient) and a sseReconnectDelay constant; the poll loop uses these in token-recovery and reconnect paths, replacing the hardcoded 3-second sleep.
Reconnect behavior test
pkg/connector/sync_test.go
Adds TestPollLoopRebuildsSSEClientAfterReconnect, which patches the helpers to simulate an io.EOF disconnect and verifies a second SSE attempt with updated access tokens and correct local revision.

Estimated code review effort: 2 (Simple) | ~12 minutes

Silent Pure-Go Channel Cache

Layer / File(s) Summary
Silent best-effort channel caching
pkg/runner.go
ChannelCreate's pure-Go channel caching now attempts creation only when key material exists and caches silently on success, removing all fmt.Printf debug branches.
Stdout capture test for ChannelCreate
pkg/runner_channel_test.go
Adds TestChannelCreateWithWASMOnlyKeyDoesNotWriteStdout and a captureStdout helper that redirects os.Stdout to verify ChannelCreate produces no stdout output.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Sequence Diagram(s)

sequenceDiagram
  participant pollLoop
  participant getLastOpRevisionWithClient
  participant listenSSEWithClient
  pollLoop->>listenSSEWithClient: attempt SSE (first client, token A)
  listenSSEWithClient-->>pollLoop: io.EOF (disconnect)
  pollLoop->>pollLoop: sleep(sseReconnectDelay)
  pollLoop->>getLastOpRevisionWithClient: recreate client, re-seed localRev
  getLastOpRevisionWithClient-->>pollLoop: localRev
  pollLoop->>listenSSEWithClient: attempt SSE (new client, token B)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description is only a fragment and is too vague to clearly describe the change. Replace the fragment with a complete sentence that briefly states the fix scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main fix: the receive loop breaking during repeated re-authentication.
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 highest/plat-37819

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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

Comment thread pkg/connector/sync.go
var (
getLastOpRevisionWithClient = func(client *line.Client) (int64, error) {
return client.GetLastOpRevision()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit — ctx should come first: Go convention is to pass context.Context as the first parameter of a function. The underlying client.ListenSSE(ctx, localRev, handler) already follows that convention, so this wrapper reads a bit awkward. Consider reordering to func(ctx context.Context, client *line.Client, localRev int64, handler func(eventType, data string)) error for consistency.

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

🧹 Nitpick comments (3)
pkg/connector/sync_test.go (1)

15-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't cover concurrent re-auth triggering described in the PR.

This test validates that a fresh client picks up a refreshed token across a sequential reconnect (EOF → new token → second attempt). It doesn't simulate the "re-auth triggered more than once at the same time" scenario from the PR title — e.g., overlapping token-recovery calls from getLastOpRevisionWithClient's recovery path and the SSE loop's recovery path, or concurrent recoverToken invocations from another goroutine. Consider adding a case that exercises overlapping/duplicate recovery triggers to directly cover the regression this PR claims to fix.

🤖 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 `@pkg/connector/sync_test.go` around lines 15 - 63, The current test in
pollLoop only covers a sequential reconnect path and misses the concurrent
re-auth race described in the PR. Extend
TestPollLoopRebuildsSSEClientAfterReconnect (or add a companion test) to
simulate overlapping recovery triggers from pollLoop,
getLastOpRevisionWithClient, and listenSSEWithClient so multiple recoverToken
calls can run at the same time. Verify LineClient.pollLoop still rebuilds the
SSE client correctly and all attempts see the refreshed token instead of only
validating the simple EOF-then-retry flow.
pkg/runner.go (1)

484-491: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent failure removes observability into pure-Go cache misses.

Errors from ltsm.NewChannel are now fully discarded with no trace. Since this cache is best-effort, complete silence is reasonable for individual failures, but consider a debug-level zerolog log (per repo convention) so persistent failures are still discoverable without reverting to fmt.Printf spam.

♻️ Suggested tweak
 	if goKey, ok := r.goKeys[keyID]; ok && goKey.privKey != nil {
 		goChan, err := ltsm.NewChannel(goKey.privKey, peerPubBytes)
-		if err == nil {
-			r.goChannels[id] = goChan
+		if err != nil {
+			log.Debug().Err(err).Int("key_id", keyID).Msg("pure-Go channel cache creation failed")
+		} else {
+			r.goChannels[id] = goChan
 		}
 	}

As per coding guidelines, "Use zerolog for logging throughout the codebase."

🤖 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 `@pkg/runner.go` around lines 484 - 491, The pure-Go cache path in runner.go
silently drops failures from ltsm.NewChannel, which makes repeated cache misses
hard to discover. Keep the best-effort behavior in r.goChannels, but in the
goKey/privKey block add a debug-level zerolog log when NewChannel returns an
error, using the existing logger pattern in this package so failures are
observable without changing control flow.

Source: Coding guidelines

pkg/runner_channel_test.go (1)

60-79: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Pipe writer isn't closed if fn() panics.

If fn() panics, defer restores os.Stdout but writePipe is never closed, leaving the io.Copy goroutine at Lines 59-62 blocked forever. Low practical risk here since fn only calls ChannelCreate, but worth hardening with a defer for the pipe close/goroutine drain.

🤖 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 `@pkg/runner_channel_test.go` around lines 60 - 79, The stdout capture in the
helper around fn(), os.Stdout, and writePipe is not panic-safe because writePipe
is only closed after fn returns normally, which can leave the io.Copy goroutine
blocked forever if fn panics. Move the pipe close and goroutine drain into a
defer alongside restoring oldStdout so the writer is always closed and the done
channel is always read, even when ChannelCreate panics.
🤖 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.

Nitpick comments:
In `@pkg/connector/sync_test.go`:
- Around line 15-63: The current test in pollLoop only covers a sequential
reconnect path and misses the concurrent re-auth race described in the PR.
Extend TestPollLoopRebuildsSSEClientAfterReconnect (or add a companion test) to
simulate overlapping recovery triggers from pollLoop,
getLastOpRevisionWithClient, and listenSSEWithClient so multiple recoverToken
calls can run at the same time. Verify LineClient.pollLoop still rebuilds the
SSE client correctly and all attempts see the refreshed token instead of only
validating the simple EOF-then-retry flow.

In `@pkg/runner_channel_test.go`:
- Around line 60-79: The stdout capture in the helper around fn(), os.Stdout,
and writePipe is not panic-safe because writePipe is only closed after fn
returns normally, which can leave the io.Copy goroutine blocked forever if fn
panics. Move the pipe close and goroutine drain into a defer alongside restoring
oldStdout so the writer is always closed and the done channel is always read,
even when ChannelCreate panics.

In `@pkg/runner.go`:
- Around line 484-491: The pure-Go cache path in runner.go silently drops
failures from ltsm.NewChannel, which makes repeated cache misses hard to
discover. Keep the best-effort behavior in r.goChannels, but in the
goKey/privKey block add a debug-level zerolog log when NewChannel returns an
error, using the existing logger pattern in this package so failures are
observable without changing control flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33845e41-2dbb-43cd-8c40-209048050f1d

📥 Commits

Reviewing files that changed from the base of the PR and between db69682 and f1beb43.

📒 Files selected for processing (4)
  • pkg/connector/sync.go
  • pkg/connector/sync_test.go
  • pkg/runner.go
  • pkg/runner_channel_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use go fmt for code formatting across all Go files
Use goimports with -local "github.com/highesttt/matrix-line-messenger" flag to group project-local imports correctly
Use zerolog for logging throughout the codebase
Do not use Msgf in logging; use Msg with structured fields instead
Use Stringer interface where applicable in Go code

Files:

  • pkg/connector/sync_test.go
  • pkg/runner_channel_test.go
  • pkg/runner.go
  • pkg/connector/sync.go
**/!(ltsm)/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/!(ltsm)/**/*.go: Run staticcheck on all Go files excluding pkg/ltsm package (transpiled WASM code)
Run go vet on all Go files excluding pkg/ltsm package (transpiled WASM code)

Files:

  • pkg/connector/sync_test.go
  • pkg/runner_channel_test.go
  • pkg/runner.go
  • pkg/connector/sync.go
🔇 Additional comments (3)
pkg/connector/sync.go (2)

37-46: LGTM!

Also applies to: 1163-1171, 1265-1267


1236-1237: 🚀 Performance & Scalability

Check whether newClient() is cheap and side-effect free. It now runs on every loop pass, so any network setup, connection pooling, or other side effects would add avoidable reconnect overhead.

pkg/runner_channel_test.go (1)

10-46: LGTM!

@highesttt highesttt merged commit 36ea517 into main Jul 6, 2026
10 checks passed
@highesttt highesttt deleted the highest/plat-37819 branch July 6, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant