Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 38 additions & 40 deletions Sources/DistributedActors/ActorMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

👍


public init<Failure: Error>(_ error: Failure) {
private let codableError: CodableError

public var error: Error {
self.codableError
}

enum CodingKeys: CodingKey {
case manifest
case error
}

public init<Failure: CodableError>(_ 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<Failure: Error>(_ 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)
Copy link
Member

Choose a reason for hiding this comment

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

oh interesting

}

// 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)
Copy link
Member Author

Choose a reason for hiding this comment

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

I couldn't get this approach to work because Codable is a protocol:

value of protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols

Copy link
Member

Choose a reason for hiding this comment

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

Right, what you dod though is absolutely right 👍

// } 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't seem like this change is required but adding them here to be consistent

Copy link
Member

Choose a reason for hiding this comment

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

Right yeah because we default to JSON so it works fine by using it.

Thanks for adding here 👍

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message: ActorMessage>(
public mutating func registerCodable<Message: Codable>(
Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated to the PR.

Even though ActorMessage is a type alias of Codable, using Codable explicitly here to better match method name (similar to registerProtobufRepresentable)

Copy link
Member

Choose a reason for hiding this comment

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

Right ok, thankfully I'm able to remove both those specialized functions soon :)

_ type: Message.Type, hint hintOverride: String? = nil,
serializer serializerOverride: SerializerID? = nil
) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/DistributedActors/Serialization/Serialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ public class Serialization {
settings.registerManifest(CRDT.ORSet<Int>.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
Copy link
Member

Choose a reason for hiding this comment

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

👍


self.settings = settings
self.metrics = system.metrics

Expand Down
71 changes: 71 additions & 0 deletions Tests/DistributedActorsTests/SerializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}