Skip to content

actor: add SingletonRef for service keys with one actor#10759

Draft
gijswijs wants to merge 2 commits intolightningnetwork:masterfrom
gijswijs:actor-singleton-ref
Draft

actor: add SingletonRef for service keys with one actor#10759
gijswijs wants to merge 2 commits intolightningnetwork:masterfrom
gijswijs:actor-singleton-ref

Conversation

@gijswijs
Copy link
Copy Markdown
Collaborator

Summary

Adds a SingletonRef type to the actor package for service keys that are expected to have at most one registered actor, along with ServiceKey.SpawnSingleton and ServiceKey.Singleton helpers.

SingletonRef is an ActorRef implementation that performs a direct receptionist lookup on each Tell/Ask and forwards to the single registered actor. Compared to Router + RoundRobinStrategy, it:

  • Drops the routing strategy (no allocation, no conceptual mismatch)
  • Documents the "one-actor-per-key" invariant at the API level
  • Tolerates transient double-registration by logging the violation and using the first registered actor (keeps the system functional rather than failing closed)

Public API additions

  • SingletonRef[M, R] — lookup-proxy type implementing ActorRef[M, R]
  • NewSingletonRef(...) — standalone constructor; any consumer can hold one
  • ServiceKey.SpawnSingleton(...)UnregisterAll + Spawn in one call; safe to invoke repeatedly for the same key
  • ServiceKey.Singleton(as) — convenience that returns a SingletonRef wired to the system's receptionist and DLO

Design notes

  • SpawnSingleton is not atomic across concurrent callers and not transactional on failure. Both caveats are called out in its godoc, and a TODO captures the latter as future hardening.
  • getActor picks the first registered actor when it sees len > 1, and logs the invariant violation loudly so the underlying bug is visible.

Motivation

Split out from PR #9821, which fronts a singleton RBF-close actor behind Router + RoundRobinStrategy. That works, but it's both semantically wrong (round-robin over N=1) and more ceremony than the call site needs.

To keep this PR focused on the actor package itself, it does not modify #9821. Once that PR lands, a follow-up commit on this branch (or a separate PR) can replace RbfChanCloserRouter / RoundRobinStrategy / RbfChanCloseActor in peer/rbf_close_wrapper_actor.go with SpawnSingleton / Singleton, deleting the router-specific plumbing.

The onionmessage package also uses a per-peer singleton (one actor per peer pubkey), but it already holds the ActorRef directly on Brontide rather than routing through a lookup — so it doesn't need SingletonRef. A separate follow-up could still swap its serviceKey.Spawn for SpawnSingleton to harden reconnect races (where the old actor hasn't been unregistered yet when the peer reconnects), but that's an independent concern.

Test plan

  • go test ./actor/... -race passes
  • New tests cover: no-actor Tell/Ask, basic Tell/Ask, ID format, repeated spawn replacing the prior actor, dynamic register/unregister, multiple-registration tolerance, no-DLO-configured, context cancellation, the Singleton helper, ActorRef interface satisfaction, repeated-spawn freshness via actor-ID echo
  • go vet and gofmt clean; no lines exceed the 80-column limit (tabs counted as 8)
  • A documentation example (ExampleServiceKey_SpawnSingleton) demonstrates the intended usage for godoc consumers

Introduce SingletonRef, an ActorRef implementation that acts as a
lookup proxy for service keys expected to have exactly one registered
actor. Unlike Router + RoundRobinStrategy, which is overkill (and
semantically misleading) for the "one-actor-per-key" pattern,
SingletonRef performs a direct receptionist lookup on each Tell/Ask
with no routing strategy or extra allocations.

Two ServiceKey helpers tie this together:

- SpawnSingleton unregisters any existing actor under the key and
  spawns a replacement, so it is safe to call repeatedly (e.g. when a
  per-channel actor is re-initialized). It is documented as neither
  concurrent-atomic nor transactional on registration failure, with a
  TODO to harden the latter.

- Singleton returns a SingletonRef wired to the system's receptionist
  and dead-letter office, letting consumers reach the actor without
  holding a direct ActorRef.

When more than one actor is registered (a transient race or caller
bug), SingletonRef logs the invariant violation and forwards to the
first registered actor, so the system remains functional rather than
failing closed.

Tests cover: no-actor Tell/Ask, basic Tell/Ask, ID format, repeated
spawn replacing the prior actor, dynamic register/unregister,
multiple-registration tolerance, no-DLO-configured, context
cancellation, the Singleton helper, ActorRef interface satisfaction,
and repeated-spawn freshness via actor-ID echo.
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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 introduces a specialized mechanism for managing singleton actors within the actor system. By replacing the generic Router and RoundRobinStrategy with a dedicated SingletonRef, the implementation reduces allocation overhead and enforces a clear 'one-actor-per-key' invariant. The changes provide a more semantic and efficient API for services that only require a single active instance, while also improving resilience against transient registration races.

Highlights

  • New SingletonRef Type: Introduced a SingletonRef type that acts as a lookup proxy for service keys, ensuring that messages are routed to the single registered actor without the overhead of a routing strategy.
  • Simplified Actor Lifecycle: Added ServiceKey.SpawnSingleton and ServiceKey.Singleton helpers to streamline the registration and consumption of singleton actors, including automatic handling of re-registration.
  • Robustness Improvements: Implemented tolerance for transient double-registration by logging invariant violations and defaulting to the first registered actor, keeping the system functional.

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

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.

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.

@github-actions github-actions bot added the severity-medium Focused review required label Apr 17, 2026
@github-actions
Copy link
Copy Markdown

🟡 PR Severity: MEDIUM

Classified by file path | 1 non-test file | 159 lines changed

🟡 Medium (1 file)
  • actor/singleton.go - New Go package not covered by a higher-severity category; falls under "Other Go files not categorized above"
Excluded from scoring (2 files)
  • actor/singleton_test.go - test file (*_test.go)
  • actor/example_singleton_test.go - test file (*_test.go)

Analysis

This PR introduces a new actor package with a Singleton type. The package lives at actor/, which does not match any of the CRITICAL or HIGH severity path prefixes (wallet, htlcswitch, contractcourt, sweep, peer, brontide, keychain, input, channeldb, funding, lnwire, routing, invoices, discovery, graph, watchtower, etc.). It is classified as MEDIUM under "Other Go files not categorized above".

Bump thresholds were not triggered: only 1 non-test/non-generated file changed (threshold: >20) and 159 lines added (threshold: >500 lines).


To override, add a severity-override-{critical,high,medium,low} label.
<!-- pr-severity-bot -->

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

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 singleton pattern to the actor system, providing a SingletonRef for dynamic actor lookups and a SpawnSingleton method for managing single-actor lifecycles. Feedback was provided to enhance error visibility in SpawnSingleton by logging a warning if registration fails, preventing the service from silently entering a dead state after a non-transactional replacement.

Comment thread actor/singleton.go
Comment on lines +138 to +147
func (sk ServiceKey[M, R]) SpawnSingleton(as *ActorSystem, id string,
behavior ActorBehavior[M, R],
opts ...ActorOption[M, R]) (ActorRef[M, R], error) {

// Stop and unregister any previous actor for this key so that only one
// instance exists after we return.
sk.UnregisterAll(as)

return RegisterWithSystem(as, id, sk, behavior, opts...)
}
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 SpawnSingleton method performs a destructive 'stop-then-register' sequence. As noted in the documentation, this is not transactional. If RegisterWithSystem fails (for example, if the provided id is already in use by another service), the previous actor(s) for this ServiceKey will have already been stopped and unregistered, leaving the service in a 'dead' state. It would be beneficial to log a warning when RegisterWithSystem fails in this context to provide better visibility into why a singleton service might have disappeared.

References
  1. Minimize lines for log and error messages, while adhering to the 80-character limit. (link)
  2. Ensure errors are logged effectively, especially in lifecycle operations where failure leads to an inconsistent or degraded system state.

@gijswijs gijswijs force-pushed the actor-singleton-ref branch from e26f092 to 46526db Compare April 17, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

severity-medium Focused review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant