@@ -266,7 +266,7 @@ public actor WebPushManager: Sendable {
266266 case . httpClient( let httpClient, let privateKeyProvider) :
267267 var logger = logger ?? backgroundActivityLogger
268268 logger [ metadataKey: " message " ] = " .data( \( message. base64URLEncodedString ( ) ) ) "
269- try await execute (
269+ try await encryptPushMessage (
270270 httpClient: httpClient,
271271 privateKeyProvider: privateKeyProvider,
272272 data: message,
@@ -458,7 +458,7 @@ public actor WebPushManager: Sendable {
458458 logger [ metadataKey: " message " ] = " \( message) "
459459 switch executor {
460460 case . httpClient( let httpClient, let privateKeyProvider) :
461- try await execute (
461+ try await encryptPushMessage (
462462 httpClient: httpClient,
463463 privateKeyProvider: privateKeyProvider,
464464 data: message. data,
@@ -482,14 +482,14 @@ public actor WebPushManager: Sendable {
482482 /// Send a message via HTTP Client, mocked or otherwise, encrypting it on the way.
483483 /// - Parameters:
484484 /// - httpClient: The protocol implementing HTTP-like functionality.
485- /// - applicationServerECDHPrivateKey : The private key to use for the key exchange. If nil, one will be generated.
485+ /// - privateKeyProvider : The private key to use for the key exchange. If nil, one will be generated.
486486 /// - message: The message to send as raw data.
487487 /// - subscriber: The subscriber to sign the message against.
488488 /// - deduplicationTopic: The topic to use when deduplicating messages stored on a Push Service.
489489 /// - expiration: The expiration of the message.
490490 /// - urgency: The urgency of the message.
491491 /// - logger: The logger to use for status updates.
492- func execute (
492+ func encryptPushMessage (
493493 httpClient: some HTTPClientProtocol ,
494494 privateKeyProvider: Executor . KeyProvider ,
495495 data message: some DataProtocol ,
@@ -499,6 +499,9 @@ public actor WebPushManager: Sendable {
499499 urgency: Urgency ,
500500 logger: Logger
501501 ) async throws {
502+ let clock = ContinuousClock ( )
503+ let startTime = clock. now
504+
502505 var logger = logger
503506 logger [ metadataKey: " subscriber " ] = [
504507 " vapidKeyID " : " \( subscriber. vapidKeyID) " ,
@@ -508,6 +511,11 @@ public actor WebPushManager: Sendable {
508511 logger [ metadataKey: " urgency " ] = " \( urgency) "
509512 logger [ metadataKey: " origin " ] = " \( subscriber. endpoint. origin) "
510513 logger [ metadataKey: " messageSize " ] = " \( message. count) "
514+ logger [ metadataKey: " topic " ] = " \( topic? . description ?? " nil " ) "
515+
516+ /// Force a random topic so any retries don't get duplicated.
517+ // let topic = topic ?? Topic()
518+ // logger[metadataKey: "resolvedTopic"] = "\(topic)"
511519 logger. trace ( " Sending notification " )
512520
513521 guard let signingKey = vapidKeyLookup [ subscriber. vapidKeyID] else {
@@ -589,8 +597,60 @@ public actor WebPushManager: Sendable {
589597 logger. warning ( " The message expiration should be less than \( Expiration . recommendedMaximum) seconds. " )
590598 }
591599
600+ let expirationDeadline : ContinuousClock . Instant ? = if expiration == . dropIfUndeliverable || expiration == . recommendedMaximum {
601+ nil
602+ } else {
603+ startTime. advanced ( by: . seconds( max ( expiration, . dropIfUndeliverable) . seconds) )
604+ }
605+
606+ let retryDurations : [ Duration ] = [ . milliseconds( 500 ) , . seconds( 2 ) , . seconds( 10 ) ]
607+
608+ /// Build and send the request.
609+ try await executeRequest (
610+ httpClient: httpClient,
611+ endpointURLString: subscriber. endpoint. absoluteURL. absoluteString,
612+ authorization: authorization,
613+ expiration: expiration,
614+ urgency: urgency,
615+ topic: topic,
616+ requestContent: requestContent,
617+ clock: clock,
618+ expirationDeadline: expirationDeadline,
619+ retryDurations: retryDurations [ ... ] ,
620+ logger: logger
621+ )
622+ }
623+
624+ func executeRequest(
625+ httpClient: some HTTPClientProtocol ,
626+ endpointURLString: String ,
627+ authorization: String ,
628+ expiration: Expiration ,
629+ urgency: Urgency ,
630+ topic: Topic ? ,
631+ requestContent: [ UInt8 ] ,
632+ clock: ContinuousClock ,
633+ expirationDeadline: ContinuousClock . Instant ? ,
634+ retryDurations: ArraySlice < Duration > ,
635+ logger: Logger
636+ ) async throws {
637+ var logger = logger
638+ logger [ metadataKey: " retryDurationsRemaining " ] = . array( retryDurations. map { " \( $0. components. seconds) seconds " } )
639+
640+ var expiration = expiration
641+ var requestDeadline = NIODeadline . distantFuture
642+ if let expirationDeadline {
643+ let remainingDuration = clock. now. duration ( to: expirationDeadline)
644+ expiration = Expiration ( seconds: Int ( remainingDuration. components. seconds) )
645+ requestDeadline = . now( ) + TimeAmount( remainingDuration)
646+ logger [ metadataKey: " resolvedExpiration " ] = " \( expiration) "
647+ logger [ metadataKey: " expirationDeadline " ] = " \( expirationDeadline) "
648+ }
649+
650+ logger. trace ( " Preparing to send push message. " )
651+
592652 /// Add the VAPID authorization and corrent content encoding and type.
593- var request = HTTPClientRequest ( url: subscriber . endpoint . absoluteURL . absoluteString )
653+ var request = HTTPClientRequest ( url: endpointURLString )
594654 request. method = . POST
595655 request. headers. add ( name: " Authorization " , value: authorization)
596656 request. headers. add ( name: " Content-Encoding " , value: " aes128gcm " )
@@ -603,10 +663,10 @@ public actor WebPushManager: Sendable {
603663 request. body = . bytes( ByteBuffer ( bytes: requestContent) )
604664
605665 /// Send the request to the push endpoint.
606- let response = try await httpClient. execute ( request, deadline: . distantFuture , logger: logger)
666+ let response = try await httpClient. execute ( request, deadline: requestDeadline , logger: logger)
607667 logger [ metadataKey: " response " ] = " \( response) "
608668 logger [ metadataKey: " statusCode " ] = " \( response. status) "
609- logger. trace ( " Sent notification " )
669+ logger. trace ( " Sent push message. " )
610670
611671 /// Check the response and determine if the subscription should be removed from our records, or if the notification should just be skipped.
612672 switch response. status {
@@ -615,10 +675,31 @@ public actor WebPushManager: Sendable {
615675 case . payloadTooLarge:
616676 logger. error ( " The encrypted payload was too large and was rejected by the push service. " )
617677 throw MessageTooLargeError ( )
618- // TODO: 429 too many requests, 500 internal server error, 503 server shutting down - check config and perform a retry after a delay?
678+ case . tooManyRequests, . internalServerError, . serviceUnavailable:
679+ /// 429 too many requests, 500 internal server error, 503 server shutting down are all opportunities to just retry if we can, otherwise throw the error
680+ guard let retryDuration = retryDurations. first else {
681+ logger. trace ( " Message was rejected, no retries remaining. " )
682+ throw PushServiceError ( response: response)
683+ }
684+ logger. trace ( " Message was rejected, but can be retried. " )
685+
686+ try await Task . sleep ( for: retryDuration)
687+ try await executeRequest (
688+ httpClient: httpClient,
689+ endpointURLString: endpointURLString,
690+ authorization: authorization,
691+ expiration: expiration,
692+ urgency: urgency,
693+ topic: topic,
694+ requestContent: requestContent,
695+ clock: clock,
696+ expirationDeadline: expirationDeadline,
697+ retryDurations: retryDurations. dropFirst ( ) ,
698+ logger: logger
699+ )
619700 default : throw PushServiceError ( response: response)
620701 }
621- logger. trace ( " Successfully sent notification " )
702+ logger. trace ( " Successfully sent push message. " )
622703 }
623704}
624705
0 commit comments