diff --git a/Sources/DistributedActors/ActorMessages.swift b/Sources/DistributedActors/ActorMessages.swift index 0a23980c3..f0a0df120 100644 --- a/Sources/DistributedActors/ActorMessages.swift +++ b/Sources/DistributedActors/ActorMessages.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Distributed Actors open source project // -// Copyright (c) 2018-2019 Apple Inc. and the Swift Distributed Actors project authors +// Copyright (c) 2018-2020 Apple Inc. and the Swift Distributed Actors project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -80,72 +80,70 @@ extension Result: ActorMessage where Success: ActorMessage { // FIXME: only then } /// Generic transportable Error type, can be used to wrap error types and represent them as best as possible for transporting. -// FIXME: Needs better impl: https://github.com/apple/swift-distributed-actors/issues/512 public struct ErrorEnvelope: Error, ActorMessage { - public let error: Error + public typealias CodableError = Error & Codable - public init(_ error: Failure) { + private let codableError: CodableError + + public var error: Error { + self.codableError + } + + enum CodingKeys: CodingKey { + case manifest + case error + } + + public init(_ error: Failure) { if let alreadyAnEnvelope = error as? Self { self = alreadyAnEnvelope - } else if let codableError = error as? Error & Codable { - self.error = codableError } else { - // we we can at least carry the error type (not the whole string repr, since it may have information we'd rather not share though) - self.error = BestEffortStringError(representation: String(reflecting: Failure.self)) + self.codableError = error } } - // this is a cop out if we want to send back a message or just type name etc - public init(description: String) { - self.error = BestEffortStringError(representation: description) + public init(_ error: Failure) { + // we can at least carry the error type (not the whole string repr, since it may have information we'd rather not share though) + self.codableError = BestEffortStringError(representation: String(reflecting: Failure.self)) } - enum CodingKeys: CodingKey { - case manifest - case error + // this is a cop out if we want to send back a message or just type name etc + public init(description: String) { + self.codableError = BestEffortStringError(representation: description) } public init(from decoder: Decoder) throws { -// guard let context = decoder.actorSerializationContext else { -// throw SerializationError.missingSerializationContext(ErrorEnvelope.self, details: "While decoding [\(ErrorEnvelope.self)], using [\(decoder)]") -// } + guard let context = decoder.actorSerializationContext else { + throw SerializationError.missingSerializationContext(decoder, ErrorEnvelope.self) + } let container = try decoder.container(keyedBy: CodingKeys.self) - // FIXME: implement being able to encode and carry Codable errors -// // FIXME: serialization should offer to more easily perform manifest deserialization of a Codable inside another one -// let manifest = try container.decode(Serialization.Manifest.self, forKey: .manifest) -// -// if let ErrorType = try context.summonType(from: manifest) as? Codable.Type { -// ErrorType._decode(from: &bytes, using: JSONDecoder()) -// -// self.error = error -// } else { -// throw SerializationError.unableToDeserialize(hint: "Unable to summon Codable type for \(manifest)") -// } - self.error = try container.decode(BestEffortStringError.self, forKey: .error) + let manifest = try container.decode(Serialization.Manifest.self, forKey: .manifest) + let errorType = try context.summonType(from: manifest) + + guard let codableErrorType = errorType as? CodableError.Type else { + throw SerializationError.unableToDeserialize(hint: "Error type \(errorType) is not Codable") + } + + let errorDecoder = try container.superDecoder(forKey: .error) + self.codableError = try codableErrorType.init(from: errorDecoder) } - // FIXME: this likely fails in some cases public func encode(to encoder: Encoder) throws { guard let context: Serialization.Context = encoder.actorSerializationContext else { - throw SerializationError.missingSerializationContext(encoder, self) + throw SerializationError.missingSerializationContext(encoder, ErrorEnvelope.self) } var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(context.serialization.outboundManifest(type(of: self.codableError as Any)), forKey: .manifest) - // FIXME: implement being able to encode and carry Codable errors (!) -// if let codableError = self.error as? Codable { -// try container.encode(context.outboundManifest(type(of: self.error as Any)), forKey: .manifest) -// try container.encode(codableError, forKey: .error) -// } else { - try container.encode(context.outboundManifest(BestEffortStringError.self), forKey: .manifest) - try container.encode(BestEffortStringError(representation: "\(type(of: self.error as Any))"), forKey: .error) -// } + let errorEncoder = container.superEncoder(forKey: .error) + try self.codableError.encode(to: errorEncoder) } } -public struct BestEffortStringError: Error, Codable, CustomStringConvertible { +public struct BestEffortStringError: Error, Codable, Equatable, CustomStringConvertible { let representation: String public var description: String { diff --git a/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift b/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift index ed032de4f..b4c35aaeb 100644 --- a/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift +++ b/Sources/DistributedActors/Serialization/Serialization+SerializerID.swift @@ -113,5 +113,8 @@ extension Serialization { // op log receptionist internal static let PushOps: SerializerID = .foundationJSON internal static let AckOps: SerializerID = .foundationJSON + + internal static let ErrorEnvelope: SerializerID = .foundationJSON + internal static let BestEffortStringError: SerializerID = .foundationJSON } } diff --git a/Sources/DistributedActors/Serialization/Serialization+Settings.swift b/Sources/DistributedActors/Serialization/Serialization+Settings.swift index 98d55d76f..07e355af1 100644 --- a/Sources/DistributedActors/Serialization/Serialization+Settings.swift +++ b/Sources/DistributedActors/Serialization/Serialization+Settings.swift @@ -149,7 +149,7 @@ extension Serialization.Settings { /// /// By doing this before system startup you can ensure a specific serializer is used for those messages. /// Make sure tha other nodes in the system are configured the same way though. - public mutating func registerCodable( + public mutating func registerCodable( _ type: Message.Type, hint hintOverride: String? = nil, serializer serializerOverride: SerializerID? = nil ) { diff --git a/Sources/DistributedActors/Serialization/Serialization.swift b/Sources/DistributedActors/Serialization/Serialization.swift index 97bfe78f3..97ae0ea1e 100644 --- a/Sources/DistributedActors/Serialization/Serialization.swift +++ b/Sources/DistributedActors/Serialization/Serialization.swift @@ -165,6 +165,10 @@ public class Serialization { settings.registerManifest(CRDT.ORSet.self, serializer: .protobufRepresentable) // settings.registerManifest(AnyDeltaCRDT.self, serializer: ReservedID.CRDTDeltaBox) // FIXME: so we cannot test the CRDT.Envelope+SerializationTests + // errors + settings.registerCodable(ErrorEnvelope.self) // TODO: can be removed once https://github.com/apple/swift/pull/30318 lands + settings.registerCodable(BestEffortStringError.self) // TODO: can be removed once https://github.com/apple/swift/pull/30318 lands + self.settings = settings self.metrics = system.metrics diff --git a/Tests/DistributedActorsTests/SerializationTests.swift b/Tests/DistributedActorsTests/SerializationTests.swift index 63d951b90..723cc2ab5 100644 --- a/Tests/DistributedActorsTests/SerializationTests.swift +++ b/Tests/DistributedActorsTests/SerializationTests.swift @@ -27,6 +27,7 @@ class SerializationTests: ActorSystemTestBase { settings.serialization.registerCodable(HasStringRef.self) settings.serialization.registerCodable(HasIntRef.self) settings.serialization.registerCodable(HasInterestingMessageRef.self) + settings.serialization.registerCodable(CodableTestingError.self) } } @@ -287,6 +288,66 @@ class SerializationTests: ActorSystemTestBase { } s2.shutdown().wait() } + + // ==== ------------------------------------------------------------------------------------------------------------ + // MARK: Error envelope serialization + + func test_serialize_errorEnvelope_stringDescription() throws { + let description = "BOOM!!!" + let errorEnvelope = ErrorEnvelope(description: description) + + var (manifest, bytes) = try shouldNotThrow { + try system.serialization.serialize(errorEnvelope) + } + + let back: ErrorEnvelope = try shouldNotThrow { + try system.serialization.deserialize(as: ErrorEnvelope.self, from: &bytes, using: manifest) + } + + guard let bestEffortStringError = back.error as? BestEffortStringError else { + throw self.testKit.error("\(back.error) is not BestEffortStringError") + } + + bestEffortStringError.representation.shouldEqual(description) + } + + func test_serialize_errorEnvelope_notCodableError() throws { + let notCodableError: NotCodableTestingError = .errorTwo + let errorEnvelope = ErrorEnvelope(notCodableError) + + var (manifest, bytes) = try shouldNotThrow { + try system.serialization.serialize(errorEnvelope) + } + + let back: ErrorEnvelope = try shouldNotThrow { + try system.serialization.deserialize(as: ErrorEnvelope.self, from: &bytes, using: manifest) + } + + guard let bestEffortStringError = back.error as? BestEffortStringError else { + throw self.testKit.error("\(back.error) is not BestEffortStringError") + } + + bestEffortStringError.representation.shouldContain(String(reflecting: NotCodableTestingError.self)) + } + + func test_serialize_errorEnvelope_codableError() throws { + let codableError: CodableTestingError = .errorB + let errorEnvelope = ErrorEnvelope(codableError) + + var (manifest, bytes) = try shouldNotThrow { + try system.serialization.serialize(errorEnvelope) + } + + let back: ErrorEnvelope = try shouldNotThrow { + try system.serialization.deserialize(as: ErrorEnvelope.self, from: &bytes, using: manifest) + } + + guard let codableTestingError = back.error as? CodableTestingError else { + throw self.testKit.error("\(back.error) is not CodableTestingError") + } + + codableTestingError.shouldEqual(codableError) + } } // MARK: Example types for serialization tests @@ -366,3 +427,13 @@ private struct NotSerializable { self.pos = pos } } + +private enum NotCodableTestingError: Error, Equatable { + case errorOne + case errorTwo +} + +private enum CodableTestingError: String, Error, Equatable, Codable { + case errorA + case errorB +}