+---------+
| Pusher |
+---------+
|
v
+---------+ +----------------+
| Queue |------------------------------------------->| ConnectionPool |
+---------+ +----------------+
^ |
| |
+---------------------------+ |
| v
+---------+ +------------------------+ +----------------+
| History |-------->| Error Response Checker |-------->| Connection |------------------+
+---------+ +------------------------+ +----------------+ |
^ ^ | |
| | v |
| | +---------------------------------+ |
| +-------------| Apple Push Notification Service | |
| +---------------------------------+ |
| |
| |
| |
+-------------------------------------------------------------------------------------+
So we currently have 3 problems/missing features:
- Sending messages is not thread safe
- Not receiving error responses
- Not aware of which messages Apple successfully received
I figured this layout would be a good way to solve all three. We can probably have a DispatchCenter class or something that holds on to the Queue of notifications to be sent, ConnectionPool of open connections to APNS, ErrorResponseChecker for each connection that blocks reading data on the APNS connection, and a History of notification messages.
Here's an example of the client code:
# Raises an error if we can't connect
pusher = Grocer.pusher(...)
notification = Grocer::Notification.new(...)
invalid_notification = Grocer::Notification.new(...)
notifications = [invalid_notification, notification]
# Adds the notification to the queue and immediately returns
pusher.push!(notification)
# Adds the notification to the queue and waits for it to be delivered
pusher.push(notification)
# Adds the notification to the queue, waits for it to be delivered, and
# waits an extra `ERROR_TIMEOUT` milliseconds to give APNS a chance to
# reject the notification. If a notification is rejected it rewinds to
# the position of the rejected notification and replays unsent
# notifications (since they wouldn't have been delivered).
pusher.ensure_push(notification)
# Adds a list of notifications to the queue, then waits once at the end
# once all of them have been added.
notifications.each do |notification|
pusher.push(notification)
end
pusher.ensure_delivery
# Similar to the above but waits at the end of the block
pusher.ensure_delivery do
notifications.each do |notification|
push(notification)
end
end
# I'm not sure if we need this yet, but it might also be nice to hold
# on to notifications that could not be delivered
pusher.failed_notifications # => [invalid_notification]
If we have a thread-safe queue of notifications to be sent, and another of those that were sent, I don't think we'd need a connection pool w/in Grocer - clients cold simply have multiple connections if they need, or share a single, fast connection amongst many threads. That would also let us not worry about the "batch send" thing (where we send and check plus one extra read at the end) b/c we'd always have a listening thread.
I think that would also let us keep the API small and consistent.
A back of the napkin design:
pusher.push(notification)and it is added to thesendqueue and then immediately returns (perhaps returning an:identifier?). A send-thread constantly pops off thesendqueue, writes the notification to the TCP connection, and pushes it onto thesentqueue. Theerror_response_listenerthread is always reading data from the TCP connection, re-enqueuing when an error happens.