-
Notifications
You must be signed in to change notification settings - Fork 2.3k
actor: add SingletonRef for service keys with one actor #10759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
gijswijs
wants to merge
2
commits into
lightningnetwork:master
Choose a base branch
from
gijswijs:actor-singleton-ref
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| package actor_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/lightningnetwork/lnd/actor" | ||
| "github.com/lightningnetwork/lnd/fn/v2" | ||
| ) | ||
|
|
||
| // SingletonGreetingMsg is a message type for the singleton example. | ||
| type SingletonGreetingMsg struct { | ||
| actor.BaseMessage | ||
| Name string | ||
| } | ||
|
|
||
| // MessageType implements actor.Message. | ||
| func (m SingletonGreetingMsg) MessageType() string { | ||
| return "SingletonGreetingMsg" | ||
| } | ||
|
|
||
| // ExampleServiceKey_SpawnSingleton demonstrates the singleton pattern: a | ||
| // single actor is registered under a service key, and consumers reach it | ||
| // through a SingletonRef that performs receptionist lookups on each call. | ||
| // This avoids the overhead of a Router+RoutingStrategy when there is never | ||
| // more than one actor per key. | ||
| func ExampleServiceKey_SpawnSingleton() { | ||
| system := actor.NewActorSystem() | ||
| defer func() { _ = system.Shutdown() }() | ||
|
|
||
| greeterKey := actor.NewServiceKey[SingletonGreetingMsg, string]( | ||
| "singleton-greeter", | ||
| ) | ||
|
|
||
| // The spawner registers exactly one actor for the key. Calling | ||
| // SpawnSingleton again would replace any existing actor atomically | ||
| // from the caller's perspective (stop-then-register). | ||
| behavior := actor.NewFunctionBehavior( | ||
| func(_ context.Context, | ||
| msg SingletonGreetingMsg) fn.Result[string] { | ||
|
|
||
| return fn.Ok("Hello, " + msg.Name + "!") | ||
| }, | ||
| ) | ||
| if _, err := greeterKey.SpawnSingleton( | ||
| system, "greeter-actor", behavior, | ||
| ); err != nil { | ||
| fmt.Printf("spawn failed: %v\n", err) | ||
| return | ||
| } | ||
|
|
||
| // Consumers get a SingletonRef. They don't need to know the actor | ||
| // ID or hold a direct reference — the ref resolves the actor via | ||
| // the receptionist on each Tell/Ask. | ||
| ref := greeterKey.Singleton(system) | ||
|
|
||
| for _, name := range []string{"Alice", "Bob"} { | ||
| result := ref.Ask( | ||
| context.Background(), SingletonGreetingMsg{Name: name}, | ||
| ).Await(context.Background()) | ||
| result.WhenOk(func(s string) { | ||
| fmt.Println(s) | ||
| }) | ||
| } | ||
|
|
||
| // Output: | ||
| // Hello, Alice! | ||
| // Hello, Bob! | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| package actor | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
|
|
||
| "github.com/lightningnetwork/lnd/fn/v2" | ||
| ) | ||
|
|
||
| // Compile-time assertion that SingletonRef satisfies the ActorRef interface. | ||
| var _ ActorRef[Message, any] = (*SingletonRef[Message, any])(nil) | ||
|
|
||
| // SingletonRef is an ActorRef implementation that acts as a lookup proxy for | ||
| // service keys expected to have exactly one registered actor. It holds no | ||
| // direct reference to the target actor; instead, it performs a receptionist | ||
| // lookup on each Tell/Ask and forwards to whichever actor is currently | ||
| // registered. Compared to Router, this skips the routing strategy entirely, | ||
| // which is both semantically correct for "one-actor-per-key" patterns (e.g. | ||
| // "the RBF closer for channel X") and avoids unnecessary allocations. | ||
| // | ||
| // Because SingletonRef does not own the target actor's lifecycle, spawning | ||
| // and unregistering are done through the ServiceKey, typically via | ||
| // ServiceKey.SpawnSingleton. The only ActorRef this type holds is the | ||
| // optional DLO used when no actor is registered. | ||
| type SingletonRef[M Message, R any] struct { | ||
| receptionist *Receptionist | ||
| serviceKey ServiceKey[M, R] | ||
| dlo ActorRef[Message, any] | ||
| } | ||
|
|
||
| // NewSingletonRef creates a new SingletonRef for the given service key. The | ||
| // receptionist is used to discover the actor registered under the key. The | ||
| // optional dlo is used as the destination for Tell messages sent when no | ||
| // actor is registered; if dlo is nil, such messages are dropped with a log | ||
| // warning. | ||
| func NewSingletonRef[M Message, R any](receptionist *Receptionist, | ||
| key ServiceKey[M, R], | ||
| dlo ActorRef[Message, any]) *SingletonRef[M, R] { | ||
|
|
||
| return &SingletonRef[M, R]{ | ||
| receptionist: receptionist, | ||
| serviceKey: key, | ||
| dlo: dlo, | ||
| } | ||
| } | ||
|
|
||
| // getActor performs a receptionist lookup for the singleton's service key. It | ||
| // returns ErrNoActorsAvailable if no actor is registered. If more than one | ||
| // actor is registered, it logs a warning about the invariant violation and | ||
| // returns the first registered actor, so callers remain functional during | ||
| // transient registration races. | ||
| func (s *SingletonRef[M, R]) getActor() (ActorRef[M, R], error) { | ||
| refs := FindInReceptionist(s.receptionist, s.serviceKey) | ||
| switch len(refs) { | ||
| case 0: | ||
| return nil, ErrNoActorsAvailable | ||
|
|
||
| case 1: | ||
| return refs[0], nil | ||
|
|
||
| default: | ||
| // Invariant violation: a singleton service key must have at | ||
| // most one registered actor. This can happen transiently | ||
| // during a re-registration race; log loudly so the bug is | ||
| // visible, then fall through and use the first registered | ||
| // actor to keep the system functional. | ||
| log.Warnf("SingletonRef(%s): %d actors registered for "+ | ||
| "singleton service key, expected 1; using first", | ||
| s.serviceKey.name, len(refs)) | ||
|
|
||
| return refs[0], nil | ||
| } | ||
| } | ||
|
|
||
| // Tell sends a message to the singleton actor. If no actor is currently | ||
| // registered and a DLO is configured, the message is forwarded to the DLO; | ||
| // otherwise it is dropped with a log warning, matching Router's behavior. | ||
| func (s *SingletonRef[M, R]) Tell(ctx context.Context, msg M) { | ||
| ref, err := s.getActor() | ||
| if err != nil { | ||
| if errors.Is(err, ErrNoActorsAvailable) && s.dlo != nil { | ||
| s.dlo.Tell(context.Background(), msg) | ||
| } else { | ||
| log.Warnf("SingletonRef(%s): message %s dropped "+ | ||
| "(no actor registered, no DLO configured)", | ||
| s.serviceKey.name, msg.MessageType()) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| ref.Tell(ctx, msg) | ||
| } | ||
|
|
||
| // Ask sends a message to the singleton actor and returns a Future for the | ||
| // response. If no actor is registered, the Future is completed immediately | ||
| // with ErrNoActorsAvailable. | ||
| func (s *SingletonRef[M, R]) Ask(ctx context.Context, msg M) Future[R] { | ||
| ref, err := s.getActor() | ||
| if err != nil { | ||
| promise := NewPromise[R]() | ||
| promise.Complete(fn.Err[R](err)) | ||
|
|
||
| return promise.Future() | ||
| } | ||
|
|
||
| return ref.Ask(ctx, msg) | ||
| } | ||
|
|
||
| // ID returns an identifier for this singleton reference. Since SingletonRef | ||
| // is a lookup proxy rather than a direct reference to a concrete actor, its | ||
| // ID is derived from the service key. | ||
| func (s *SingletonRef[M, R]) ID() string { | ||
| return "singleton(" + s.serviceKey.name + ")" | ||
| } | ||
|
|
||
| // SpawnSingleton registers a singleton actor under this service key. Any | ||
| // existing actors registered under the same key are unregistered and stopped | ||
| // first, so this method is safe to call repeatedly (e.g. when a channel | ||
| // closer is re-initialized for the same channel point). It returns the | ||
| // ActorRef of the newly spawned actor. | ||
| // | ||
| // NOTE: The unregister-then-register sequence is not atomic under the | ||
| // receptionist lock. Concurrent callers racing to spawn a singleton for the | ||
| // same key may temporarily leave two actors registered. SingletonRef | ||
| // tolerates this by logging and using the first registered actor. If strict | ||
| // at-most-one registration is required, callers must coordinate externally. | ||
| // | ||
| // NOTE: SpawnSingleton is also not transactional with respect to failure. | ||
| // UnregisterAll runs before RegisterWithSystem, so if the registration step | ||
| // returns an error (e.g. ErrEmptyActorID, ErrNilBehavior, | ||
| // ErrDuplicateActorID) any previously registered actor has already been | ||
| // stopped and there is no rollback. Callers must pass a valid config. | ||
| // | ||
| // TODO: make SpawnSingleton transactional by validating the config and | ||
| // reserving the actor ID before tearing down the previous registration, so a | ||
| // failed replacement leaves the existing singleton intact. | ||
| 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...) | ||
| } | ||
|
|
||
| // Singleton returns a SingletonRef that can be used to reach the actor | ||
| // registered under this service key. The returned ref does not spawn an | ||
| // actor — it performs receptionist lookups on each Tell/Ask. Combined with | ||
| // SpawnSingleton (called elsewhere to register the actor), this is the | ||
| // preferred pattern for "one-actor-per-key" services: the spawner manages | ||
| // the actor lifecycle while consumers simply hold a Singleton ref. | ||
| func (sk ServiceKey[M, R]) Singleton( | ||
| as *ActorSystem) *SingletonRef[M, R] { | ||
|
|
||
| return NewSingletonRef(as.Receptionist(), sk, as.DeadLetters()) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
SpawnSingletonmethod performs a destructive 'stop-then-register' sequence. As noted in the documentation, this is not transactional. IfRegisterWithSystemfails (for example, if the providedidis already in use by another service), the previous actor(s) for thisServiceKeywill have already been stopped and unregistered, leaving the service in a 'dead' state. It would be beneficial to log a warning whenRegisterWithSystemfails in this context to provide better visibility into why a singleton service might have disappeared.References