@@ -43,6 +43,7 @@ type AndroidNotificationServer struct {
43
43
AndroidPushSettings AndroidPushSettings
44
44
client * messaging.Client
45
45
sendTimeout time.Duration
46
+ retryTimeout time.Duration
46
47
}
47
48
48
49
// serviceAccount contains a subset of the fields in service-account.json.
@@ -56,12 +57,13 @@ type serviceAccount struct {
56
57
TokenURI string `json:"token_uri"`
57
58
}
58
59
59
- func NewAndroidNotificationServer (settings AndroidPushSettings , logger * mlog.Logger , metrics * metrics , sendTimeoutSecs int ) * AndroidNotificationServer {
60
+ func NewAndroidNotificationServer (settings AndroidPushSettings , logger * mlog.Logger , metrics * metrics , sendTimeoutSecs int , retryTimeoutSecs int ) * AndroidNotificationServer {
60
61
return & AndroidNotificationServer {
61
62
AndroidPushSettings : settings ,
62
63
metrics : metrics ,
63
64
logger : logger ,
64
65
sendTimeout : time .Duration (sendTimeoutSecs ) * time .Second ,
66
+ retryTimeout : time .Duration (retryTimeoutSecs ) * time .Second ,
65
67
}
66
68
}
67
69
@@ -168,21 +170,13 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus
168
170
},
169
171
}
170
172
171
- ctx , cancel := context .WithTimeout (context .Background (), me .sendTimeout )
172
- defer cancel ()
173
-
174
173
me .logger .Info (
175
174
"Sending android push notification" ,
176
175
mlog .String ("device" , me .AndroidPushSettings .Type ),
177
176
mlog .String ("type" , msg .Type ),
178
177
mlog .String ("ack_id" , msg .AckID ),
179
178
)
180
-
181
- start := time .Now ()
182
- _ , err := me .client .Send (ctx , fcmMsg )
183
- if me .metrics != nil {
184
- me .metrics .observerNotificationResponse (PushNotifyAndroid , time .Since (start ).Seconds ())
185
- }
179
+ err := me .SendNotificationWithRetry (fcmMsg )
186
180
187
181
if err != nil {
188
182
errorCode , hasStatusCode := getErrorCode (err )
@@ -240,6 +234,78 @@ func (me *AndroidNotificationServer) SendNotification(msg *PushNotification) Pus
240
234
return NewOkPushResponse ()
241
235
}
242
236
237
+ func (me * AndroidNotificationServer ) SendNotificationWithRetry (fcmMsg * messaging.Message ) error {
238
+ var err error
239
+ waitTime := time .Second
240
+
241
+ logger := me .logger .With (mlog .String ("did" , fcmMsg .Token ))
242
+
243
+ // Keep a general context to make sure the whole retry
244
+ // doesn't take longer than the timeout.
245
+ generalContext , cancelGeneralContext := context .WithTimeout (context .Background (), me .sendTimeout )
246
+ defer cancelGeneralContext ()
247
+
248
+ for retries := 0 ; retries < MAX_RETRIES ; retries ++ {
249
+ start := time .Now ()
250
+
251
+ retryContext , cancelRetryContext := context .WithTimeout (generalContext , me .retryTimeout )
252
+ defer cancelRetryContext ()
253
+ _ , err = me .client .Send (retryContext , fcmMsg )
254
+ if me .metrics != nil {
255
+ me .metrics .observerNotificationResponse (PushNotifyAndroid , time .Since (start ).Seconds ())
256
+ }
257
+
258
+ if err == nil || ! isRetryable (err ) {
259
+ break
260
+ }
261
+
262
+ logger .Error (
263
+ "Failed to send android push" ,
264
+ mlog .Int ("retry" , retries ),
265
+ mlog .Err (err ),
266
+ )
267
+
268
+ if retries == MAX_RETRIES - 1 {
269
+ logger .Error ("Max retries reached" )
270
+ break
271
+ }
272
+
273
+ select {
274
+ case <- generalContext .Done ():
275
+ if generalContext .Err () != nil {
276
+ logger .Info (
277
+ "Not retrying because context error" ,
278
+ mlog .Int ("retry" , retries ),
279
+ mlog .Err (generalContext .Err ()),
280
+ )
281
+ }
282
+ return generalContext .Err ()
283
+ case <- time .After (waitTime ):
284
+ }
285
+
286
+ waitTime *= 2
287
+ }
288
+
289
+ return err
290
+ }
291
+
292
+ func isRetryable (err error ) bool {
293
+ // We retry if the context deadline is exceeded.
294
+ // This may cause double notifications, but we expect
295
+ // this not to happen often.
296
+ if errors .Is (err , context .DeadlineExceeded ) {
297
+ return true
298
+ }
299
+
300
+ // We retry the errors based on https://firebase.google.com/docs/cloud-messaging/http-server-ref
301
+ return messaging .IsInternal (err ) ||
302
+ messaging .IsQuotaExceeded (err )
303
+
304
+ // messaging.IsUnavailable is retried by the default retry config in
305
+ // firebase.google.com/go/[email protected] /internal/http_client.go
306
+ // messaging.IsUnavailable(err)
307
+ }
308
+
243
309
func getErrorCode (err error ) (string , bool ) {
244
310
if err == nil {
245
311
return "" , false
0 commit comments