Skip to content

Forward the SSO-Check node's session and checkpoint reads to the Session node - #4401

Merged
madurangasiriwardena merged 1 commit into
thunder-id:mainfrom
madurangasiriwardena:fix/sso-reuse-duplicate-reads
Jul 30, 2026
Merged

Forward the SSO-Check node's session and checkpoint reads to the Session node#4401
madurangasiriwardena merged 1 commit into
thunder-id:mainfrom
madurangasiriwardena:fix/sso-reuse-duplicate-reads

Conversation

@madurangasiriwardena

@madurangasiriwardena madurangasiriwardena commented Jul 28, 2026

Copy link
Copy Markdown
Member

Purpose

An SSO session reuse issued six statements against SSO_SESSION / SSO_SESSION_CONTEXT for four rows' worth of work, because the two nodes that make up a reuse each read the same rows independently:

  1. The SSO-Check node resolves the session by handle (SSO-SESS-02).
  2. It then answers "does this session hold this checkpoint?" by listing every checkpoint id of the session and matching in Go (SSO-SESS-08).
  3. The paired Session node re-reads the same session by the same handle (SSO-SESS-02 again), microseconds later in the same request.
  4. It then fetches the checkpoint context by full primary key (SSO-SESS-06).

Neither repeat read validated anything: liveness, state, both deadlines, flow id and flow version are all decided by the resolver in step 1, and the Session node's re-read only detected a vanished row. The listing in step 2 is a prefix range scan that answers a question the full-key fetch in step 4 answers by itself, and its cost grows with the number of checkpoints a flow defines.

This PR hands both rows from the check node to the Session node, so a reuse issues four statements instead of six: one SSO_SESSION read instead of two, one SSO_SESSION_CONTEXT read instead of two.

Measured on PostgreSQL with pg_stat_statements (track_planning=on, counters reset per run) and the correlation-id query log:

Before After
Reuse leg, all statements 20 18
Reuse leg, SSO session statements 6 4
SSO_SESSION reads per reuse 2 1
SSO_SESSION_CONTEXT reads per reuse 2 1
Round trips removed 2

The removed statements measured 0.121 ms (0.096 planning + 0.025 execution) for the checkpoint listing and roughly 0.086 ms for the duplicate session read, against 0.433 ms of total server-side time for the whole reuse path. Note that planning dominates every read on this path, at 2x to 4x its execution time, because each statement is sent as an unnamed prepared statement and re-planned per call. The 0.024 ms full-key probe that replaces the 0.121 ms listing is the same query the load path already used.

No user-facing behaviour changes. No SQL syntax was added, so nothing is engine-specific.

Approach

The rows travel on ForwardedData, the engine's existing node-to-node channel. The SSO-Check node puts the session it resolved and the context it fetched on its ExecutorResponse; the engine promotes them onto the next node's context; the Session node reads them and passes them to the service. No new plumbing, and the handover is visible in the graph rather than hidden in a side channel.

Forwarded only on the Skip outcome. ForwardedData reaches the immediate next node and is then cleared, and it is a serialized field. On the Authenticate outcome the flow prompts and suspends, so anything left there could be persisted with the flow context. The Skip outcome is the only path where the Session node runs next, so it is the only path that forwards. assertAbsent asserts ForwardedData is empty across all three Authenticate cases.

Validated on arrival, not trusted. The service rejects a forwarded session unless its HANDLE_ID matches the handle being loaded, and a forwarded context unless both its session id and checkpoint id match what is being restored. Anything rejected or absent is read from the store, so a Session node reached without the handover still works and a partial handover costs a query rather than correctness.

Interface changes (both internal to internal/flow, nothing product-facing):

  • Service.HasCheckpoint(ctx, sessionID, checkpoint) (bool, error) becomes FindCheckpoint(...) (*SessionContext, error). Fetching the row by its full primary key answers the availability question by itself (!= nil) and gives the check node something to forward.
  • Service.LoadCheckpoint takes a LoadCheckpointInput struct rather than growing to seven positional parameters, mirroring the existing SaveCheckpointInput. The two forwarded rows are optional fields on it.

These regenerate internal/flow/session/Service_mock_test.go and, because Service is consumed from internal/flow/executor, the shared tests/mocks/flow/sessionmock/Service_mock.go. That accounts for the mock diff.

Dead code removed. sessionStore.ListCheckpointIDs, its store implementation and queryListCheckpointsBySessionID (SSO-SESS-08) have no callers left and are deleted rather than left as unused SQL.

Known limitation, deliberate. ForwardedData reaches the immediate next node only, so the handover applies when the Session node follows its SSO-Check node directly. The shipped BASIC_SSO template wires onSuccess to the same node as checkpointRef, and the console always emits that shape, so this holds in practice. It is not enforced: validateSSOCheckExecutor checks that checkpointRef names an existing SessionExecutor node, but not that the success edge points at it. A flow authored through the management API with a node in between simply falls back to two reads, so correctness is unaffected and only the saving is lost. Worth noting separately that the same unenforced invariant has a pre-existing consequence beyond this PR: such a flow would take the Skip outcome and never reach the Session node, so the snapshot would never be restored. That validation gap is a follow-up, not part of this change.

Related Issues

  • N/A

Related PRs

  • N/A

Checklist

  • Followed the contribution guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
    • Ran Vale and fixed all errors and warnings
  • Tests provided. (Add links if there are any)
    • Unit Tests
    • Integration Tests
  • Breaking changes. (Fill if applicable)
    • Breaking changes section filled.
    • breaking change label added.

Security checks

  • Followed secure coding standards in WSO2 Secure Coding Guidelines
  • Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets.

Summary by CodeRabbit

Summary by CodeRabbit

  • Performance Improvements

    • Reduced redundant database reads by reusing forwarded SSO session and checkpoint context during session checkpoint loading.
  • Bug Fixes

    • Updated checkpoint detection to rely on the retrieved checkpoint context snapshot, improving routing correctness.
    • Improved behavior when checkpoint data is missing or lookup fails, ensuring flows fail or skip reliably.
  • Tests

    • Updated and expanded tests to cover forwarded reuse, absent/present checkpoint handling, and lookup error propagation.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7523cc7d-bd0d-45df-babd-07c37c711c6e

📥 Commits

Reviewing files that changed from the base of the PR and between d6079bd and 7c81869.

⛔ Files ignored due to path filters (1)
  • backend/tests/mocks/flow/sessionmock/Service_mock.go is excluded by !**/*_mock.go
📒 Files selected for processing (13)
  • backend/internal/flow/common/constants.go
  • backend/internal/flow/executor/session_executor.go
  • backend/internal/flow/executor/session_executor_test.go
  • backend/internal/flow/executor/sso_check_executor.go
  • backend/internal/flow/executor/sso_check_executor_test.go
  • backend/internal/flow/session/Service_mock_test.go
  • backend/internal/flow/session/interface.go
  • backend/internal/flow/session/service.go
  • backend/internal/flow/session/service_test.go
  • backend/internal/flow/session/sessionStore_mock_test.go
  • backend/internal/flow/session/session_context_store_test.go
  • backend/internal/flow/session/store.go
  • backend/internal/flow/session/store_constants.go
💤 Files with no reviewable changes (5)
  • backend/internal/flow/session/store_constants.go
  • backend/internal/flow/session/interface.go
  • backend/internal/flow/session/sessionStore_mock_test.go
  • backend/internal/flow/session/store.go
  • backend/internal/flow/session/session_context_store_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/internal/flow/common/constants.go

📝 Walkthrough

Walkthrough

The SSO checkpoint flow now uses direct checkpoint-context lookup, forwards resolved session data between paired executors, and accepts structured checkpoint-load inputs that reuse matching forwarded rows.

Changes

SSO checkpoint forwarding

Layer / File(s) Summary
Persistence contract cleanup
backend/internal/flow/session/interface.go, backend/internal/flow/session/store.go, backend/internal/flow/session/store_constants.go, backend/internal/flow/session/*_test.go
Checkpoint ID enumeration is removed from the session store interface, implementation, SQL constants, mocks, and tests.
Checkpoint service contract and loading
backend/internal/flow/session/service.go, backend/internal/flow/session/service_test.go, backend/internal/flow/session/Service_mock_test.go
FindCheckpoint returns checkpoint context directly, while LoadCheckpointInput carries request metadata and optional forwarded session/context rows for validated reuse.
Executor handoff
backend/internal/flow/common/constants.go, backend/internal/flow/executor/sso_check_executor.go, backend/internal/flow/executor/session_executor.go, backend/internal/flow/executor/*_test.go
SSO-Check forwards resolved rows and Session passes them into checkpoint loading; tests cover forwarded, absent, mismatched, and error paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SSOCheckExecutor
  participant SessionExecutor
  participant SessionService
  participant SessionStore
  SSOCheckExecutor->>SessionService: FindCheckpoint(sessionID, checkpoint)
  SessionService->>SessionStore: GetByCheckpoint(sessionID, checkpoint)
  SessionStore-->>SessionService: SessionContext
  SessionService-->>SSOCheckExecutor: checkpoint context
  SSOCheckExecutor->>SessionExecutor: Forward Session and SessionContext
  SessionExecutor->>SessionService: LoadCheckpoint(LoadCheckpointInput)
  SessionService-->>SessionExecutor: Session and SessionContext
Loading

Possibly related PRs

  • thunder-id/thunderid#3907: Introduced the session-store ListCheckpointIDs surface that this change removes in favor of direct checkpoint lookup.
  • thunder-id/thunderid#4300: Also modifies the SSO session checkpoint load and participant-handling path.

Suggested reviewers: thamindudilshan, darshanasbg, rajithacharith

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: forwarding SSO-Check session and checkpoint data to the Session node.
Description check ✅ Passed The description follows the template and includes Purpose, Approach, Related Issues/PRs, Checklist, and Security checks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 unit tests (beta)
  • Create PR with unit tests

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
Contributor

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 `@backend/internal/flow/session/transport.go`:
- Around line 65-71: Update WithInbound to preserve the existing non-nil
InboundHandle.memo when wrapping an inbound context, allocating a new
ssoReadMemo only when no memo is present; continue storing the resulting handle
with context.WithValue.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 70c2533b-6039-49d2-b3eb-a826a5eb970b

📥 Commits

Reviewing files that changed from the base of the PR and between d46470e and 6031064.

📒 Files selected for processing (9)
  • backend/internal/flow/session/interface.go
  • backend/internal/flow/session/service.go
  • backend/internal/flow/session/service_test.go
  • backend/internal/flow/session/sessionStore_mock_test.go
  • backend/internal/flow/session/session_context_store_test.go
  • backend/internal/flow/session/store.go
  • backend/internal/flow/session/store_constants.go
  • backend/internal/flow/session/transport.go
  • backend/internal/flow/session/transport_test.go
💤 Files with no reviewable changes (5)
  • backend/internal/flow/session/store_constants.go
  • backend/internal/flow/session/sessionStore_mock_test.go
  • backend/internal/flow/session/interface.go
  • backend/internal/flow/session/store.go
  • backend/internal/flow/session/session_context_store_test.go

Comment thread backend/internal/flow/session/transport.go Outdated
@madurangasiriwardena
madurangasiriwardena force-pushed the fix/sso-reuse-duplicate-reads branch 3 times, most recently from 845d923 to d6079bd Compare July 28, 2026 13:57
@madurangasiriwardena madurangasiriwardena changed the title Reuse the SSO-Check node's session and checkpoint reads on the Session node Forward the SSO-Check node's session and checkpoint reads to the Session node Jul 28, 2026
@madurangasiriwardena madurangasiriwardena added the trigger-pr-builder Add when the PR is ready for CI; starts the PR Builder for this and all later pushes label Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.45283% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/internal/flow/session/service.go 85.18% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@ThaminduDilshan
ThaminduDilshan added this pull request to the merge queue Jul 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 30, 2026
…ion node

An SSO reuse issued six SSO_SESSION/SSO_SESSION_CONTEXT statements for four
rows' worth of work. The SSO-Check node resolved the session and answered
"does this checkpoint exist" by listing every checkpoint id, then the paired
Session node re-read the same session by the same handle and fetched the
checkpoint context, both microseconds later in the same request. Neither
repeat read validated anything the resolver had not already checked.

Hand both rows to the Session node through ForwardedData, the engine's
existing node-to-node channel, so the load path uses what the check node
already read. LoadCheckpoint takes them on its input struct and validates
each before use: the session is rejected unless its handle matches the one
being loaded, the context unless its session id and checkpoint id both match.
Anything rejected or absent is read from the store, so a Session node reached
without the handover still works and a partial handover costs a query rather
than correctness.

ForwardedData reaches the immediate next node only, so the rows are forwarded
on the Skip outcome alone. The Authenticate outcome prompts and suspends the
flow, and ForwardedData is a serialized field, so data left there could be
persisted with the flow context.

HasCheckpoint becomes FindCheckpoint, returning the checkpoint context instead
of a bool. Fetching the row by its full primary key answers the availability
question by itself and gives the check node something to forward, so
ListCheckpointIDs and SSO-SESS-08 have no callers left and are removed.

Measured on PostgreSQL with pg_stat_statements: the reuse leg drops from six
statements to four (one SSO_SESSION read instead of two, one
SSO_SESSION_CONTEXT read instead of two), removing two round trips and the
0.121 ms the checkpoint listing cost, which was a prefix range scan against
the 0.024 ms full-key probe that replaces it. Verified end to end: the whole
reuse leg falls from 20 statements to 18 with SSO-SESS-08 absent from
pg_stat_statements, and 250 concurrent reuses across 25 workers complete with
no failures. No SQL was added, so nothing is engine-specific.
@madurangasiriwardena
madurangasiriwardena added this pull request to the merge queue Jul 30, 2026
Merged via the queue into thunder-id:main with commit f264505 Jul 30, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trigger-pr-builder Add when the PR is ready for CI; starts the PR Builder for this and all later pushes Type/Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants