From ea0f8ab5f02c025bed33712c8515e033a2190b60 Mon Sep 17 00:00:00 2001 From: Amit Uttam Date: Tue, 19 May 2020 14:48:55 -0300 Subject: [PATCH] MM-22181 Backoff retries for ID-loaded push notification messages (#4302) * MM-22181 Backoff retries for ID-loaded push notification messages Fibonacci backoffs. 6 attempts in ~20 seconds, not including response latency for each failure. 0 seconds to attempt 1 (original try) 1 seconds to attempt 2 2 seconds to attempt 3 3 seconds to attempt 4 5 seconds to attempt 5 8 seconds to attempt 6 * PR review: Reset re-request counter when server call succeeds * PR Review: Localize scope of recursion counter This handles scenario where multiple notification requests are being made at the same time for multiple messages. Counter will be isolated for each fetch request. * PR Review: Remove unnecessary class variable * Trigger Build Co-authored-by: Miguel Alatzar --- .../mattermost/rnbeta/ReceiptDelivery.java | 53 +++++++++----- .../NotificationService.swift | 72 +++++++++++++------ 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java index ef85532c0..157772a47 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java @@ -26,6 +26,8 @@ import com.mattermost.react_native_interface.ResolvePromise; public class ReceiptDelivery { static final String CURRENT_SERVER_URL = "@currentServerUrl"; + private static final int[] FIBONACCI_BACKOFFS = new int[] { 0, 1, 2, 3, 5, 8 }; + public static void send(Context context, final String ackId, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) { final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); @@ -95,26 +97,39 @@ public class ReceiptDelivery { .post(body) .build(); - try { - Response response = client.newCall(request).execute(); - String responseBody = response.body().string(); - if (response.code() != 200 || !isIdLoaded) { - throw new Exception(responseBody); - } - JSONObject jsonResponse = new JSONObject(responseBody); - Bundle bundle = new Bundle(); - String keys[] = new String[] {"post_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"}; - for (int i = 0; i < keys.length; i++) { - String key = keys[i]; - if (jsonResponse.has(key)) { - bundle.putString(key, jsonResponse.getString(key)); - } - } - promise.resolve(bundle); - } catch (Exception e) { - Log.e("ReactNative", "Receipt delivery failed to send"); - promise.reject("Receipt delivery failure", e.toString()); + makeServerRequest(client, request, isIdLoaded, 0, promise); + } + } + + private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) { + try { + Response response = client.newCall(request).execute(); + String responseBody = response.body().string(); + if (response.code() != 200 || !isIdLoaded) { + throw new Exception(responseBody); } + JSONObject jsonResponse = new JSONObject(responseBody); + Bundle bundle = new Bundle(); + String keys[] = new String[]{"post_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"}; + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + if (jsonResponse.has(key)) { + bundle.putString(key, jsonResponse.getString(key)); + } + } + promise.resolve(bundle); + } catch (Exception e) { + Log.e("ReactNative", "Receipt delivery failed to send"); + try { + reRequestCount++; + if (reRequestCount < FIBONACCI_BACKOFFS.length) { + Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFFS[reRequestCount] + " seconds"); + Thread.sleep(FIBONACCI_BACKOFFS[reRequestCount] * 1000); + makeServerRequest(client, request, isIdLoaded, reRequestCount, promise); + } + } catch(InterruptedException ie) {} + + promise.reject("Receipt delivery failure", e.toString()); } } } diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index e460b52aa..cb916bcd0 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -5,6 +5,7 @@ class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? + var sendFailed = false; override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler @@ -14,40 +15,71 @@ class NotificationService: UNNotificationServiceExtension { let type = bestAttemptContent.userInfo["type"] let postId = bestAttemptContent.userInfo["post_id"] let idLoaded = bestAttemptContent.userInfo["id_loaded"] ?? false + UploadSession.shared.notificationReceipt( notificationId: ackId, - receivedAt: Date().millisencondsSince1970, + receivedAt: Date().millisecondsSince1970, type: type, postId: postId, idLoaded: idLoaded as! Bool ) { data, error in if (idLoaded as! Bool) { guard let data = data, error == nil else { - return - } - - let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any] - if let json = json { - if let message = json["message"] as? String { - bestAttemptContent.body = message - } - if let channelName = json["channel_name"] as? String { - bestAttemptContent.title = channelName - } - - let userInfoKeys = ["channel_name", "team_id", "sender_id", "root_id", "override_username", "override_icon_url", "from_webhook"] - for key in userInfoKeys { - if let value = json[key] as? String { - bestAttemptContent.userInfo[key] = value + self.sendFailed = true; + let fibonacciBackoffsInSeconds = [1.0, 2.0, 3.0, 5.0, 8.0] + for backoffInSeconds in fibonacciBackoffsInSeconds { + let timer = Timer.scheduledTimer(withTimeInterval: backoffInSeconds, repeats: false) { timer in + UploadSession.shared.notificationReceipt( + notificationId: ackId, + receivedAt: Date().millisecondsSince1970, + type: type, + postId: postId, + idLoaded: idLoaded as! Bool + ) { data, error in + guard let data = data, error == nil else { + self.sendFailed = true; + return + } + self.processResponse(data: data, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler) + } + } + + if (self.sendFailed) { + self.sendFailed = false + timer.fire() + } else { + break } } + return } + self.processResponse(data: data, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler) } - - contentHandler(bestAttemptContent) } } } + + func processResponse(data: Data, bestAttemptContent: UNMutableNotificationContent, contentHandler: ((UNNotificationContent) -> Void)?) { + let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any] + if let json = json { + if let message = json["message"] as? String { + bestAttemptContent.body = message + } + if let channelName = json["channel_name"] as? String { + bestAttemptContent.title = channelName + } + + let userInfoKeys = ["channel_name", "team_id", "sender_id", "root_id", "override_username", "override_icon_url", "from_webhook"] + for key in userInfoKeys { + if let value = json[key] as? String { + bestAttemptContent.userInfo[key] = value + } + } + } + if let contentHandler = contentHandler { + contentHandler(bestAttemptContent) + } + } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. @@ -60,7 +92,7 @@ class NotificationService: UNNotificationServiceExtension { } extension Date { - var millisencondsSince1970: Int { + var millisecondsSince1970: Int { return Int((self.timeIntervalSince1970 * 1000.0).rounded()) }