Skip to content

Added Cloudwatch Event #48

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

Merged
merged 6 commits into from
Mar 30, 2020
Merged

Added Cloudwatch Event #48

merged 6 commits into from
Mar 30, 2020

Conversation

fabianfett
Copy link
Member

No description provided.

@fabianfett fabianfett requested a review from tomerd March 25, 2020 09:42
public enum Cloudwatch {
public struct Event<Detail: Decodable>: Decodable {
public let id: String
public let detailType: String
Copy link
Contributor

@tomerd tomerd Mar 25, 2020

Choose a reason for hiding this comment

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

is this a fixed list that then corresponds to the Detail type? maybe there is a way to express this in a more type safe way?

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, found the list of all detail options that are available, right now:

https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#schedule_event_type

Quite a list, I'd say. So I guess the most correct way would be to implement an

enum Payload {
  case .scheduled
  case .codepipeline(CodePipelineStateChange)
  case .ec2stateChange(EC2StateChange)
  
}

detailType and detail could be completely removed thanks to the enum, different Cloudwatch events could be handled by one lambda. 🎉

But this would mean that we need to support as many detail types as possible. Extending a defined enum with more cases is sadly not possible. That's why everything we don't define upfront, can not be used by the user. But I don't know if we want to support such a massive list of potential subtypes just from the beginning (keeping in mind semver).

Neither Go, nor Java implements the complete show.

Sadly Codable doesn't support some kind of AnyCodable as Go does. That's why I think the current solutions is the best we can do so far, because it's by far the easiest to extend for custom use cases?

wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

interesting dilemma, agreed we need something that can scale or iow we implement the top ~5 common ones and the users can extend to more exotic ones. here is one way:

public enum Cloudwatch {
    /// CloudWatch.Event is the outer structure of an event sent via CloudWatch Events.
    ///
    /// **NOTE**: For examples of events that come via CloudWatch Events, see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html
    public struct Event: Decodable {
        public let id: String
        public let source: String
        public let accountId: String
        public let time: Date
        public let region: AWSRegion
        public let resources: [String]

        public let payload: Payload

        enum CodingKeys: String, CodingKey {
            case id
            case detailType = "detail-type"
            case source
            case accountId = "account"
            case time
            case region
            case resources
            case detail
        }
        
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            
            self.id = try container.decode(String.self, forKey: .id)
            self.source = try container.decode(String.self, forKey: .source)
            self.accountId = try container.decode(String.self, forKey: .accountId)
            let time = try container.decode(ISO8601Coding.self, forKey: .time)
            self.time = time.wrappedValue
            self.region = try container.decode(AWSRegion.self, forKey: .region)
            self.resources = try container.decode([String].self, forKey: .resources)
            
            let detailType = try container.decode(String.self, forKey: .detailType)
            let detailsDecoder = try container.superDecoder(forKey: .detail)
            let detailsFactory: (Decoder) throws -> Decodable
            switch detailType {
            case Payload.type.scheduled.rawValue:
                detailsFactory = Empty.init
            case Payload.type.codepipeline.rawValue:
                detailsFactory = CodePipelineStateChange.init
            case Payload.type.ec2stateChange.rawValue:
                detailsFactory = EC2StateChange.init
            default:
                guard let factory = Payload.registry[detailType] else {
                    throw UnknownPayload()
                }
                detailsFactory = factory
            }
            self.payload = Payload(label: detailType, body: try detailsFactory(detailsDecoder))
        }
    }

    public struct Payload {
        enum type: String {
            case scheduled = "Scheduled Event"
            case codepipeline
            case ec2stateChange
        }
        
        public let label: String
        public let body: Decodable
        
        public init(label: String, body: Decodable) {
            self.label = label
            self.body = body
        }
        
        // FIXME: make thread safe
        static var registry = [String: (Decoder) throws -> Decodable]()
        public static func register<T: Decodable>(label: String, type: T.Type) {
            registry[label] = type.init
        }
    }
   
    public struct Empty: Decodable {}
    
    public struct CodePipelineStateChange: Decodable {
        let foo: String
    }
    
    public struct EC2StateChange: Decodable {
        let bar: String
    }
    
    struct UnknownPayload: Error {}
}

}
}

public struct ScheduledEvent: Codable {}
Copy link
Contributor

@tomerd tomerd Mar 25, 2020

Choose a reason for hiding this comment

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

this looks a bit funny since its got no actual members, maybe a generic empty one? or have details be optional? or some other better idea?


private static let dateFormatter = ISO8601DateFormatter()
}

Copy link
Contributor

Choose a reason for hiding this comment

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

do we need a unit test for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Those are included in the test-events. So I don't think so? If you want me to I'm happy to provide extra tests though.

Copy link
Contributor

Choose a reason for hiding this comment

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

would nice to have a small unit test for each PW

Copy link
Member Author

Choose a reason for hiding this comment

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

tests added.

guard let event = maybeEvent else {
XCTFail("Expected to have an event")
return
}
Copy link
Contributor

@tomerd tomerd Mar 25, 2020

Choose a reason for hiding this comment

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

so I spoke with the XCTest team and they told me that XCTest has improved the way they handle errors so that throwing tests are no longer considered "harmful". their current recommendation is to make the test throwable and drop the XCTAssertNoThrow and do not use try/catch(XCTFail)

Copy link
Contributor

Choose a reason for hiding this comment

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

oh hmmm, they only fixed in on Darwin, looking to get the fix ported to Linux too

@fabianfett fabianfett force-pushed the cloudwatch-event branch 4 times, most recently from 5e94817 to 732157a Compare March 27, 2020 15:19
case scheduled
case ec2InstanceStateChangeNotification(EC2.InstanceStateChangeNotification)
case ec2SpotInstanceInterruptionWarning(EC2.SpotInstanceInterruptionNotice)
case custom(label: String, detail: Decodable)
Copy link
Member Author

Choose a reason for hiding this comment

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

@tomerd What do you think about going with an enum here for the Details?

Copy link
Contributor

Choose a reason for hiding this comment

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

works for me

Copy link
Contributor

Choose a reason for hiding this comment

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

btw if we are worried about breaking compatibility we could use a struct with static vars/func (eg: https://github.com/swift-server/async-http-client/blob/master/Sources/AsyncHTTPClient/HTTPClient.swift#L723) so we can add more "cases" without major version bump

case detail
}

// FIXME: make thread safe
Copy link
Member Author

Choose a reason for hiding this comment

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

@tomerd do we need to be thread safe here? In normal usage this should only be used during cold start. I think a comment should be sufficient. wdyt?

Copy link
Contributor

@tomerd tomerd Mar 27, 2020

Choose a reason for hiding this comment

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

I think the question is if we can trust the users to not do crazy things, life taught me to not trust :D but you may have a different experience. if we decided to guard, a read/write lock could be the right thing since its going to be vastly read

}

public struct CodePipelineStateChange: Decodable {
let foo: String
Copy link
Contributor

Choose a reason for hiding this comment

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

copy/pasta?

/// **NOTE**: For examples of events that come via CloudWatch Events, see
/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html
/// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
public struct Event<Detail: Decodable>: Decodable {
Copy link
Member Author

Choose a reason for hiding this comment

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

If we use a generic approach here, we don't need the factory register stuff further down. Let's use the generic type as the factory! We need to decide which detail payload we get at compile time anyway. But what we are currently going for additionally is checking whether detailType matches the Detail.Type.

But for this I would do the following:

protocol CloudwatchDetail: Decodable {
  static var name: String
}

public enum Cloudwatch {
     /// CloudWatch.Event is the outer structure of an event sent via CloudWatch Events.
     ///
     /// **NOTE**: For examples of events that come via CloudWatch Events, see
     /// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html
     /// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
     public struct Event<Detail: CloudwatchDetail>: Decodable {

     }
}

@fabianfett
Copy link
Member Author

@tomerd

okay, we are back to the generic approach, which I think is for the best 😉. I like it. I went further and used the generic approach to remove the registry. I figured it doesn’t make much sense to have a runtime registry if we have a compile time registry anyway (thanks to the generic type). Further this way we don't need synchronize the registry, which saves us from linking Dispatch or NIOConcurrencyHelpers here! 🥳

I created a new protocol:

public protocol CloudwatchDetail: Decodable {
   static var name: String { get }
}

I use the new protocol to constrain the generic type

public struct Event<Detail: CloudwatchDetail>: Decodable {}

I check within the init(from decoder: ) if the types align

public init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.id = try container.decode(String.self, forKey: .id)
  self.source = try container.decode(String.self, forKey: .source)
  self.accountId = try container.decode(String.self, forKey: .accountId)
  self.time = (try container.decode(ISO8601Coding.self, forKey: .time)).wrappedValue
  self.region = try container.decode(AWSRegion.self, forKey: .region)
  self.resources = try container.decode([String].self, forKey: .resources)
  
  let detailType = try container.decode(String.self, forKey: .detailType)
  guard detailType == Detail.name else { // <--- type check
      throw PayloadTypeMismatch(name: detailType, type: Detail.self)
  }
  self.detail = try container.decode(Detail.self, forKey: .detail)
}

I commited the changes. Revert them if you don’t like them, but I think this makes the solution easier to use, as well as to extend (implement protocol). I especially like the typealiases, which hide the generics.

}
}

struct UnknownPayload: Error {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove now?

self.resources = try container.decode([String].self, forKey: .resources)

let detailType = try container.decode(String.self, forKey: .detailType)
guard detailType == Detail.name else {
Copy link
Contributor

Choose a reason for hiding this comment

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

make this case insensitive?

Copy link
Contributor

@tomerd tomerd left a comment

Choose a reason for hiding this comment

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

I like this allot. two tiny comments and I think this is ready to go

@tomerd tomerd merged commit 3fad2f5 into master Mar 30, 2020
@tomerd tomerd deleted the cloudwatch-event branch March 30, 2020 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants