Class PushSpool

java.lang.Object
com.iizix.server.push.engine.PushSpool

public final class PushSpool extends Object
The write-ahead spool: the undelivered notifications, as a live PNSpoolerRoot tree mirrored to one XML file.

One live tree, persisted at chosen moments. A send that cannot complete adds an entry; a retry that succeeds removes it; the tree is written to disk by persistIfDirty() - which the engine calls from its replay loop, after a batch and at shutdown - never once per event. Persisting is clone-then-write: the live tree is cloned inside the framework's monitors and the clone is written outside them, which is consistent only because a PNSpoolEntry is immutable once added; a retry is therefore remove-then-add-new, never an in-place edit.

Replay order and TTL. On load the framework sorts the entries by creation time, and everything past its ttlDeadline is dropped and counted. A retry is scheduled with exponential backoff - BACKOFF_BASE_MILLIS doubled per attempt up to BACKOFF_MAX_MILLIS, or the push service's Retry-After when longer - and an entry whose next attempt would fall past its deadline is dropped as expired rather than rescheduled. FCM answers 429 RESOURCE_EXHAUSTED under load and expects exactly this.

The target is a key, not an endpoint. An entry stores the subscription's identity (SubscriptionRecord.spoolKey()); the replay looks the row up again and sends to the endpoint or token it has then, so a device that re-registered while its notification waited receives it at its new endpoint.

At-least-once: a crash between a successful send and the next persist replays that notification once more. A duplicate Web Push is annoying, not dangerous, and was ruled acceptable; nothing here assumes exactly-once.

Author:
Christopher Mindus
  • Field Details

    • FILE_NAME

      public static final String FILE_NAME
      The file name in the spool directory.
      See Also:
    • BACKOFF_BASE_MILLIS

      public static final long BACKOFF_BASE_MILLIS
      The first retry delay: 30 seconds.
      See Also:
    • BACKOFF_MAX_MILLIS

      public static final long BACKOFF_MAX_MILLIS
      The retry delay ceiling: one hour.
      See Also:
  • Constructor Details

    • PushSpool

      public PushSpool(File file, PushSpool.Storage storage)
      Constructor. Nothing is read until
      invalid reference
      #load()
      .
      Parameters:
      file - The spool file.
      storage - The XML storage.
  • Method Details

    • getFile

      public File getFile()
      Gets the file.
      Returns:
      The spool file.
    • load

      public int load(ZonedDateTime now)
      Loads the spool file, dropping expired entries. A missing file is an empty spool; a file that cannot be read is logged and left in place, and the spool starts empty rather than the server refusing to start.
      Parameters:
      now - The current time.
      Returns:
      The number of entries loaded and still valid.
    • add

      public PNSpoolEntry add(PushMessage message, String targetKey, ZonedDateTime now, ZonedDateTime ttlDeadline, long minDelayMillis) throws PropException
      Adds a pending delivery for the first retry.
      Parameters:
      message - The notification.
      targetKey - The subscription's spool key.
      now - The current time, the entry's creation time.
      ttlDeadline - When the notification expires.
      minDelayMillis - A delay the push service asked for, 0 for none.
      Returns:
      The entry, or null when even the first retry would fall past the deadline (the entry is then not added and counts as expired).
      Throws:
      PropException - If the entry cannot be built or added.
    • retry

      public PNSpoolEntry retry(PNSpoolEntry entry, ZonedDateTime now, long minDelayMillis) throws PropException
      Reschedules an entry after a failed attempt: removes it and adds a new one with the retry count incremented and the next attempt after the backoff.
      Parameters:
      entry - The entry that failed.
      now - The current time.
      minDelayMillis - A delay the push service asked for, 0 for none.
      Returns:
      The new entry, or null when the next attempt would fall past the deadline (the old entry is removed and nothing replaces it).
      Throws:
      PropException - If the entry cannot be rebuilt.
    • remove

      public void remove(PNSpoolEntry entry) throws PropException
      Removes an entry that was delivered or can never be.
      Parameters:
      entry - The entry.
      Throws:
      PropException - If the removal is refused.
    • due

      public List<PNSpoolEntry> due(ZonedDateTime now)
      Gets the entries due for an attempt, oldest first, after dropping the expired ones.
      Parameters:
      now - The current time.
      Returns:
      The due entries.
    • nextDue

      public ZonedDateTime nextDue()
      Gets the earliest next-attempt time.
      Returns:
      The time, or null when the spool is empty.
    • size

      public int size()
      Gets the number of pending entries.
      Returns:
      The count.
    • backoff

      public static long backoff(int retryCount)
      Computes the backoff for an attempt.
      Parameters:
      retryCount - The number of attempts made so far.
      Returns:
      The delay in milliseconds: base doubled per attempt, capped.
    • persistIfDirty

      public boolean persistIfDirty()
      Writes the tree when it changed since the last write: clone under the tree's monitors, write the clone outside them.
      Returns:
      true if a write happened.
    • persist

      public void persist()
      Writes the tree.