def sendNotification()

in notificationworkerlambda/src/main/scala/com/gu/notifications/worker/delivery/apns/ApnsClient.scala [44:113]


  def sendNotification(notificationId: UUID, token: String, payload: Payload, dryRun: Boolean)
    (onComplete: Either[DeliveryException, Success] => Unit)
    (implicit ece: ExecutionContextExecutor): Unit = {

    val pushNotification = new SimpleApnsPushNotification(
      TokenUtil.sanitizeTokenString(token),
      config.bundleId,
      payload.jsonString,
      //Default to no invalidation time but an hour for breaking news and 10 mins for football
      //See https://stackoverflow.com/questions/12317037/apns-notifications-ttl
      payload.ttl.map(invalidationTime(_).toDate).orNull,
      payload.deliveryPriority,
      payload.collapseId.orNull
    )

    type Feedback = PushNotificationFuture[SimpleApnsPushNotification, PushNotificationResponse[SimpleApnsPushNotification]]

    def responseHandler(start: Instant) = new PushNotificationResponseListenerWithTimeout[SimpleApnsPushNotification]() {

      def timeout(): Unit = {
        logger.info(Map(
          "worker.individualRequestLatency" -> Duration.between(start, Instant.now).toMillis,
          "notificationId" -> notificationId,
        ), "Individual send request timed out")
        onComplete(Left(FailedRequest(notificationId, token, new TimeoutException("No APNs response received in time"), Some("ClientTimeout"))))
      }

      override def operationCompleteWithoutTimeout(feedback: Feedback): Unit = {
        logger.info(Map(
          "worker.individualRequestLatency" -> Duration.between(start, Instant.now).toMillis,
          "notificationId" -> notificationId,
        ), "Individual send request completed")
        if (feedback.isSuccess) {
          val response = feedback.getNow
          if (response.isAccepted) {
            onComplete(Right(ApnsDeliverySuccess(token, Instant.now())))
          } else {
            val invalidationTimestamp = Option(response.getTokenInvalidationTimestamp)
              .map(d => new Timestamp(d.getTime).toLocalDateTime)

            val error = if (invalidationTimestamp.isDefined || invalidTokenErrorCodes.contains(response.getRejectionReason)) {
              InvalidToken(notificationId, token, response.getRejectionReason, invalidationTimestamp)
            } else {
              FailedDelivery(notificationId, token, response.getRejectionReason)
            }
            onComplete(Left(error))
          }
        } else {
          val debug =
            s"""Failed Request
               |isSuccess: ${feedback.isSuccess}, isDone: ${feedback.isDone}, isCancelled: ${feedback.isCancelled}
               |getNow: ${Option(feedback.getNow)}
               |cause: ${feedback.cause()}
               |""".stripMargin
          logger.error(debug)
          onComplete(Left(FailedRequest(notificationId, token, feedback.cause())))
        }
      }
    }

    if(dryRun) {
      onComplete(Right(ApnsDeliverySuccess(token, Instant.now(), dryRun = true)))
    } else {
      val start = Instant.now
      val futureResult = underlying.sendNotification(pushNotification)
      val handler = responseHandler(start)
      handler.startTimeout(timer, timeoutInMs = 20000L)
      futureResult.addListener(handler)
    }
  }