-
Notifications
You must be signed in to change notification settings - Fork 80
Dead letter when remote call target is dead #989
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -876,13 +876,12 @@ extension ClusterSystem { | |
| return nil | ||
| } | ||
|
|
||
| return self.namingLock.withLock { | ||
| return try self.namingLock.withLock { | ||
| guard let managed = self._managedDistributedActors.get(identifiedBy: id) else { | ||
| log.trace("Resolved as remote reference", metadata: [ | ||
| "actor/id": "\(id)", | ||
| ]) | ||
| // TODO(distributed): throw here, this should be a dead letter | ||
| return nil | ||
| throw DeadLetterError(recipient: id) | ||
| } | ||
|
|
||
| if let resolved = managed as? Act { | ||
|
|
@@ -1130,11 +1129,6 @@ extension ClusterSystem { | |
| return | ||
| } | ||
|
|
||
| guard let actor = self.resolve(id: recipient) else { | ||
| self.log.error("Unable to resolve recipient \(recipient). Message will be dropped: \(invocation)") | ||
| return | ||
| } | ||
|
|
||
| Task { | ||
| var decoder = ClusterInvocationDecoder(system: self, message: invocation) | ||
|
|
||
|
|
@@ -1148,6 +1142,12 @@ extension ClusterSystem { | |
| ) | ||
|
|
||
| do { | ||
| guard let actor = self.resolve(id: recipient) else { | ||
| self.log.error("Unable to resolve recipient \(recipient). Message will be dropped: \(invocation)") | ||
yim-lee marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.deadLetters.tell(DeadLetter(invocation, recipient: recipient)) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the dead letter happen at sender, after getting back
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the recipient I believe, since the recipient will has no function that will throw, so this dead letter informs that system that "someone is calling things on you, but they can't get through to the recipient actor". And the caller has the function that they called that resulted in a dead letter, that function should throw as it received s the error reply from the dead-letter-ed remoteCall. This way the caller knows it happened, we don't need to log it there. |
||
| throw DeadLetterError(recipient: recipient) | ||
| } | ||
|
|
||
| try await executeDistributedTarget( | ||
| on: actor, | ||
| target: target, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ | |
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import Distributed | ||
| import Foundation | ||
yim-lee marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import Logging | ||
|
|
||
| // ==== ---------------------------------------------------------------------------------------------------------------- | ||
|
|
@@ -255,3 +257,10 @@ extension ActorPath { | |
| static let _dead: ActorPath = try! ActorPath(root: "dead") | ||
| static let _deadLetters: ActorPath = try! ActorPath._dead.appending("letters") | ||
| } | ||
|
|
||
| // ==== ---------------------------------------------------------------------------------------------------------------- | ||
| // MARK: Errors | ||
|
|
||
| public struct DeadLetterError: DistributedActorSystemError, Codable { | ||
| public let recipient: ClusterSystem.ActorID | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import Distributed | ||
| @testable import DistributedActors | ||
| import DistributedActorsTestKit | ||
| @testable import Logging | ||
|
|
@@ -80,4 +81,82 @@ final class DeadLetterTests: ClusterSystemXCTestCase { | |
| try self.logCapture.awaitLogContaining(self.testKit, text: "This is a question") | ||
| try self.logCapture.awaitLogContaining(self.testKit, text: "/user/ludwig") | ||
| } | ||
|
|
||
| func test_remoteCallTerminatedTarget_shouldResultInDeadLetter() async throws { | ||
| let local = await setUpNode("local") { settings in | ||
| settings.enabled = true | ||
| } | ||
| let remote = await setUpNode("remote") { settings in | ||
| settings.enabled = true | ||
| } | ||
| local.cluster.join(node: remote.cluster.uniqueNode) | ||
|
|
||
| var greeter: Greeter? = Greeter(actorSystem: local) | ||
| let localGreeter = try Greeter.resolve(id: greeter!.id, using: remote) | ||
yim-lee marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| let p = self.testKit.makeTestProbe(expecting: String.self) | ||
| let watcher = GreeterWatcher(probe: p, actorSystem: local) | ||
| try await watcher.watch(greeter!) | ||
|
|
||
| greeter = nil | ||
| try p.expectMessage(prefix: "Received terminated: /user/Greeter") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh we're missing API to We'd want to be able to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. Created #990
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! |
||
|
|
||
| let error = try await shouldThrow { | ||
| _ = try await localGreeter.greet(name: "world") | ||
| } | ||
|
|
||
| guard error is DeadLetterError else { | ||
| throw self.testKit.fail("Expected DeadLetterError, got \(error)") | ||
| } | ||
|
|
||
| try self.capturedLogs(of: local).awaitLogContaining(self.testKit, text: "was not delivered to") | ||
| } | ||
|
|
||
| func test_resolveTerminatedTarget_shouldResultInDeadLetter() async throws { | ||
| var greeter: Greeter? = Greeter(actorSystem: self.system) | ||
| let greeterID = greeter!.id | ||
|
|
||
| let p = self.testKit.makeTestProbe(expecting: String.self) | ||
| let watcher = GreeterWatcher(probe: p, actorSystem: self.system) | ||
| try await watcher.watch(greeter!) | ||
|
|
||
| greeter = nil | ||
| try p.expectMessage(prefix: "Received terminated: /user/Greeter") | ||
|
|
||
| let error = try shouldThrow { | ||
| _ = try self.system.resolve(id: greeterID, as: Greeter.self) | ||
| } | ||
|
|
||
| guard error is DeadLetterError else { | ||
| throw self.testKit.fail("Expected DeadLetterError, got \(error)") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private distributed actor Greeter { | ||
| typealias ActorSystem = ClusterSystem | ||
|
|
||
| distributed func greet(name: String) -> String { | ||
| "hello \(name)!" | ||
| } | ||
| } | ||
|
|
||
| private distributed actor GreeterWatcher: LifecycleWatch { | ||
| typealias ActorSystem = ClusterSystem | ||
|
|
||
| let probe: ActorTestProbe<String> | ||
|
|
||
| init(probe: ActorTestProbe<String>, actorSystem: ActorSystem) { | ||
| self.actorSystem = actorSystem | ||
| self.probe = probe | ||
| } | ||
|
|
||
| distributed func watch(_ greeter: Greeter) { | ||
| watchTermination(of: greeter) | ||
| } | ||
|
|
||
| // FIXME(distributed): Should not need to be distributed: https://github.com/apple/swift/pull/59397 | ||
| public distributed func terminated(actor id: ActorID) async { // not REALLY distributed... | ||
| self.probe.tell("Received terminated: \(id)") | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
IIUC, only local resolve would reach here? In other words, it's not part of remote call code path?