Skip to content

oor: add unified OOR service for outgoing + incoming orchestration - #118

Closed
bhandras wants to merge 3 commits into
oor-client-split-5-unroll-packages-draftfrom
oor-client-split-6-oor-service-design-draft
Closed

oor: add unified OOR service for outgoing + incoming orchestration#118
bhandras wants to merge 3 commits into
oor-client-split-5-unroll-packages-draftfrom
oor-client-split-6-oor-service-design-draft

Conversation

@bhandras

@bhandras bhandras commented Feb 18, 2026

Copy link
Copy Markdown
Member

Summary

This split introduces a high-level OORService API on top of existing OOR
primitives so callers no longer manually orchestrate:

  • outgoing actor commands,
  • incoming receive-session lifecycle,
  • outbox follow-up recursion,
  • recipient-cursor progression.

The service provides one cohesive surface for:

  1. outgoing transfer lifecycle,
  2. incoming transfer sync (once + background worker),
  3. unroll package resolution delegation.

Base: oor-client-split-5-unroll-packages-draft.

Architecture Alignment

This PR now supports the same actor-system wiring pattern used by VTXO/round:

  • outgoing OOR can be resolved via actor-system service key,
  • service sends Ask to ActorRef instead of hard-wiring direct calls,
  • direct local actor construction remains available as a compatibility fallback.

Concretely:

  • added ActorServiceKey(actorID) in oor/service_key.go,
  • added ServiceConfig.ActorSystem, ServiceConfig.OutgoingServiceKey, and
    ServiceConfig.OutgoingRef,
  • NewOORService resolves outgoing in this order:
    1. OutgoingRef (explicit injection),
    2. actor-system service-key lookup,
    3. local NewOORClientActor fallback (requires DeliveryStore).

This keeps integration flexible while matching the system-wide actor plumbing
model when actor system wiring is available.

Problem This Split Solves

By split 5, OOR primitives were correct, but app/harness integration still
required low-level choreography in multiple places:

  • direct actor command handling for outgoing,
  • ad-hoc receive-session driving for incoming,
  • manual cursor discipline,
  • custom worker-loop lifecycle handling.

This split centralizes that orchestration in one tested runtime service.

What Was Added

1) Service contracts and adapters

  • oor/service_types.go
    • new OORService interface
    • typed request/status/state view structs
    • IncomingEventSource, IncomingCursorStore, UnrollPackageResolver
    • actor-system outgoing wiring config fields
  • oor/service_db_adapters.go
    • DBIncomingCursorStore adapter from
      db.OORArtifactPersistenceStore to IncomingCursorStore
  • oor/service_key.go
    • actor-system service-key helper for outgoing OOR actor lookup

2) Service runtime

  • oor/service.go
    • NewOORService(cfg ServiceConfig) constructor
    • outgoing facade:
      • StartOutgoing
      • GetOutgoingState
    • incoming orchestration:
      • SyncIncomingOnce
      • StartIncomingSync
      • StopIncomingSync
      • GetIncomingSyncStatus
    • unroll facade:
      • ResolveUnrollPackages
    • lifecycle:
      • Stop
    • outgoing actor selection supports actor-system lookup and local fallback

3) Service-level tests

  • oor/service_test.go
    • outgoing happy path through service API
    • outgoing flow through actor-system service-key wiring
    • constructor failure when actor-system lookup has no registered actor
    • incoming sync success and cursor advancement
    • ack failure does not advance cursor
    • resolver passthrough
    • background worker lifecycle start/stop

Runtime Architecture

Outgoing path

OORService.StartOutgoing -> configured outgoing actor endpoint:

  • explicit OutgoingRef, or
  • actor-system ServiceKey.Ref(...), or
  • local durable OORClientActor fallback.

Service callers use GetOutgoingState for typed state view.

Incoming path

SyncIncomingOnce (or worker cycle):

  1. list owned receive scripts,
  2. load per-script cursor,
  3. fetch events afterEventID,
  4. for each event:
    • validate ordering + payload shape,
    • drive receive FSM (IncomingTransferEvent),
    • execute outbox through local persistence/transport boundary,
    • require terminal ReceiveCompleted,
    • only then write cursor to event_id.

Flow:

script -> cursor -> events -> validate -> materialize -> ack -> cursor++

Cursor safety invariant:

  • cursor is not advanced on partial failure,
  • retries are at-least-once and converge via idempotent handlers.

Scope Notes

  • This split does not add new server RPC/stream endpoints.
  • Incoming source remains pluggable (IncomingEventSource), so a future
    long-poll or stream source can be injected without changing service API.

Testing

Executed locally:

  • go test ./...
  • make lint
  • make commitmsg-lint range=origin/main..HEAD

All green.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @bhandras, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the OOR (Outgoing/Incoming Orchestration and Resolution) system by introducing a unified OORService. This service aims to simplify the integration of OOR primitives into applications by centralizing complex choreography, such as managing actor messages, FSM sessions, and cursor progression. The change provides a more robust and easier-to-use API, ensuring consistent handling of outgoing transfers, incoming event processing, and package resolution, thereby reducing boilerplate and potential for integration errors.

Highlights

  • Unified OOR Service API: Introduced a high-level OORService API to abstract away low-level orchestration details for outgoing actor messages, incoming receive FSM sessions, outbox handler recursion, and recipient-cursor progression.
  • Cohesive Transfer Lifecycle Management: The new service provides a single, cohesive interface for managing the outgoing transfer lifecycle, handling incoming transfer synchronization (both one-time and background worker), and delegating unroll package resolution.
  • Centralized Incoming Sync Logic: Incoming synchronization now centralizes cursor correctness, automates outbox follow-up events, and standardizes the background loop lifecycle, reducing repetitive integration code and potential for errors.
  • New Service Contracts and Runtime: Added oor/service_types.go for interfaces and data structures, oor/service_db_adapters.go for database integration, and oor/service.go for the core service runtime implementation.
  • Comprehensive Service-Level Tests: Included new tests in oor/service_test.go to cover outgoing happy paths, incoming sync success and cursor advancement, ack failure scenarios, resolver passthrough, and background worker lifecycle.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • oor/service.go
    • Implemented the core oorService struct and its methods, providing the main runtime logic for outgoing transfer initiation, state retrieval, incoming event synchronization, and unroll package resolution.
    • Added NewOORService constructor to configure and initialize the service with necessary dependencies.
    • Included background worker logic for continuous incoming synchronization, with start/stop/status management.
  • oor/service_db_adapters.go
    • Introduced DBIncomingCursorStore to adapt the existing db.OORArtifactPersistenceStore for use with the new IncomingCursorStore interface.
    • Provided methods for listing owned receive scripts, retrieving recipient cursors, and upserting cursor states in the database.
  • oor/service_test.go
    • Added unit tests for TestOORServiceOutgoingFlow to verify outgoing transfer initiation and state retrieval.
    • Included TestOORServiceSyncIncomingOnce to confirm correct incoming event processing and cursor advancement.
    • Implemented TestOORServiceSyncIncomingAckFailureDoesNotAdvanceCursor to ensure cursor safety on partial failures.
    • Added tests for TestOORServiceResolveUnrollPackages to validate resolver passthrough and TestOORServiceIncomingWorkerLifecycle for background worker management.
  • oor/service_types.go
    • Defined the OORService interface, outlining the high-level API for orchestration.
    • Introduced ServiceConfig for service configuration, and various request/response/status structs like StartOutgoingRequest, OutgoingStateView, and IncomingSyncStatus.
    • Declared interfaces such as IncomingEventSource, IncomingCursorStore, and UnrollPackageResolver to enable pluggable components for incoming event handling and package resolution.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new OORService to abstract away the complexities of outgoing and incoming OOR orchestration. The new service provides a clean, high-level API, and the implementation is well-structured with comprehensive tests. The changes significantly improve the usability and maintainability of the OOR features. I have a couple of suggestions regarding context handling and the choice of random number generator for performance and robustness.

Comment thread oor/service.go Outdated
return nil
}

loopCtx, cancel := context.WithCancel(context.Background())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The worker's context is created from context.Background(), which detaches its lifecycle from the ctx passed to StartIncomingSync (which is ignored on line 178). This can be misleading and may lead to the worker not being stopped when the caller's context is canceled. To ensure proper lifecycle management and prevent potential resource leaks, the worker's context should be derived from the parent context passed to the function. You will also need to remove the _ = ctx on line 178.

Suggested change
loopCtx, cancel := context.WithCancel(context.Background())
loopCtx, cancel := context.WithCancel(ctx)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, fixed.

StartIncomingSync now derives the worker context from the provided ctx (context.WithCancel(ctx)) instead of context.Background(), with a nil-context guard that falls back to context.Background().

This ties worker lifetime to caller cancellation while preserving explicit StopIncomingSync shutdown behavior.

Comment thread oor/service.go Outdated
Comment on lines +591 to +597
jitterMax := big.NewInt(int64(s.incomingPollJitter) + 1)
noise, err := cryptorand.Int(cryptorand.Reader, jitterMax)
if err != nil {
return delay
}

jitter := time.Duration(noise.Int64())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using crypto/rand for generating jitter in a polling delay is unnecessary and can have performance implications, as it can block if the system is low on entropy. For non-security-critical randomness like this, math/rand is more appropriate and performant.

To implement this, you can add a rand *rand.Rand field to the oorService struct, initialize it in NewOORService with rand.New(rand.NewSource(time.Now().UnixNano())), and then use it here to generate the jitter.

Suggested change
jitterMax := big.NewInt(int64(s.incomingPollJitter) + 1)
noise, err := cryptorand.Int(cryptorand.Reader, jitterMax)
if err != nil {
return delay
}
jitter := time.Duration(noise.Int64())
jitterVal := s.rand.Int63n(int64(s.incomingPollJitter) + 1)
jitter := time.Duration(jitterVal)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied.

The jitter path now uses a non-crypto PRNG (math/rand) stored on the service struct (jitterRand) instead of crypto/rand.

This keeps the polling delay jitter fast and avoids blocking entropy reads for non-security randomness.

@bhandras
bhandras force-pushed the oor-client-split-6-oor-service-design-draft branch from 126bb02 to 3ee98c4 Compare February 18, 2026 18:50
Comment thread oor/service.go Outdated
return SessionID{}, fmt.Errorf("service must be provided")
}

result := s.actor.Receive(ctx, &StartTransferRequest{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why direct recv on actor vs adding it to the system and using a service key to get a normal ref?

@bhandras bhandras Feb 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I took a closer look at the serverconn/mailbox plumbing direction in PR #116, and I think your point stands: if we route incoming through that connector path, the incoming orchestration I added here is likely unnecessary.

Given that direction, this PR should probably be treated as temporary scaffolding (or closed) rather than the long-term shape.

Comment thread oor/service.go
}

// GetOutgoingState returns a caller-facing state summary for one session.
func (s *oorService) GetOutgoingState(ctx context.Context,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So then what ends up driving this? Thought we had a like coordinator actor that would talk to the vtxo actors when things need to happen based on the wallet or server?

@bhandras bhandras Feb 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. After looking at PR #116 more closely, the intended model is to drive incoming server events through the unified connector/mailbox path into actors, not through a service-local polling loop.

So the incoming orchestration in this PR is likely redundant with that target architecture.

Comment thread oor/service.go
}
defer session.FSM.Stop()

outbox, err := askFSMEvent(ctx, session.FSM, &IncomingTransferEvent{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here. Does this need to be a durable actor? Then can makes sure stuff like send/recv OOR is properly restarted.

@bhandras bhandras Feb 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — after reviewing PR #116, I agree the long-term durable restart story should come from mailbox/serverconn ingress + actor dispatch, not this service-level incoming loop.

Given that, the incoming receive path in this PR is probably not worth carrying forward as-is. We can close this and reintroduce only the pieces that still fit once the connector plumbing lands.

@bhandras
bhandras force-pushed the oor-client-split-6-oor-service-design-draft branch from 3ee98c4 to de8b95e Compare February 19, 2026 06:36
@bhandras
bhandras force-pushed the oor-client-split-5-unroll-packages-draft branch 5 times, most recently from 715a781 to 1c40b23 Compare February 20, 2026 16:17
Define a high-level OORService API surface for outgoing orchestration,
incoming sync, and unroll package resolution. The new contracts include
incoming event-source and cursor-store abstractions so runtime code can
stay decoupled from specific transport and persistence implementations.

Add a DBIncomingCursorStore adapter that maps OOR artifact store script
and cursor rows into the service interfaces.
Build the concrete OOR service implementation on top of the durable
outgoing actor and the receive-side FSM. The service now runs incoming
script polling, validates cursor/event ordering, drives materialize+ack
flows, and only advances cursors after successful processing.

The implementation also adds lifecycle controls for background incoming
sync workers and exposes typed outgoing state and unroll resolver
facades.
Add service-level tests for outgoing happy-path orchestration,
incoming sync cursor progression, ack-failure cursor safety, resolver
passthrough, and worker start/stop behavior.

These tests lock in the key split-6 guarantees: incoming cursors only
move after materialize+ack succeeds and callers can drive both outgoing
and incoming behavior through one high-level API.
@bhandras
bhandras force-pushed the oor-client-split-6-oor-service-design-draft branch from de8b95e to 49e1ff2 Compare February 20, 2026 16:38
@bhandras

Copy link
Copy Markdown
Member Author

Closing this as the direction is already set in #116 and therefore this won't be necessary or not in this current form. We can always revisit once we're in the plumbing stage.

@bhandras bhandras closed this Feb 20, 2026
ellemouton added a commit that referenced this pull request Mar 17, 2026
rounds: purify FSM transitions via outbox handler pattern
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.

2 participants