Class PushSpool
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
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceReads and writes the tree as XML: the server's property factory in production, a test's own in a test.Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longThe first retry delay: 30 seconds.static final longThe retry delay ceiling: one hour.static final StringThe file name in the spool directory.Constructor Summary
ConstructorsMethod Summary
Modifier and TypeMethodDescriptionadd(PushMessage message, String targetKey, ZonedDateTime now, ZonedDateTime ttlDeadline, long minDelayMillis) Adds a pending delivery for the first retry.static longbackoff(int retryCount) Computes the backoff for an attempt.due(ZonedDateTime now) Gets the entries due for an attempt, oldest first, after dropping the expired ones.getFile()Gets the file.intload(ZonedDateTime now) Loads the spool file, dropping expired entries.nextDue()Gets the earliest next-attempt time.voidpersist()Writes the tree.booleanWrites the tree when it changed since the last write: clone under the tree's monitors, write the clone outside them.voidremove(PNSpoolEntry entry) Removes an entry that was delivered or can never be.retry(PNSpoolEntry entry, ZonedDateTime now, long minDelayMillis) 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.intsize()Gets the number of pending entries.
Field Details
FILE_NAME
BACKOFF_BASE_MILLIS
public static final long BACKOFF_BASE_MILLISThe first retry delay: 30 seconds.- See Also:
BACKOFF_MAX_MILLIS
public static final long BACKOFF_MAX_MILLISThe retry delay ceiling: one hour.- See Also:
Constructor Details
PushSpool
Constructor. Nothing is read until.invalid reference
#load()- Parameters:
file- The spool file.storage- The XML storage.
Method Details
getFile
load
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
Removes an entry that was delivered or can never be.- Parameters:
entry- The entry.- Throws:
PropException- If the removal is refused.
due
Gets the entries due for an attempt, oldest first, after dropping the expired ones.- Parameters:
now- The current time.- Returns:
- The due entries.
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.