Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions src/main/java/network/crypta/client/async/USKAttempt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package network.crypta.client.async;

import network.crypta.keys.ClientSSKBlock;
import network.crypta.keys.USK;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Tracks a single edition probe, including its checker state and polling metadata.
*
* <p>Each attempt owns a {@link USKChecker} that performs the actual request and reports completion
* through {@link USKCheckerCallback}. The attempt records whether it has succeeded, failed (DNF),
* or been canceled, and it exposes scheduling hooks used by the owning fetcher. The attempt also
* tracks whether it has ever entered finite cooldown so that polling rounds can determine when a
* round is finished for now.
*
* <p>The class is mutable and relies on synchronization for checker state updates. Callers usually
* treat each attempt as part of a larger scheduling loop, invoking {@link #schedule(ClientContext)}
* and reacting to callbacks from the checker. Instances are short-lived and are replaced as polling
* rounds advance.
*
* <ul>
* <li>Owns a checker for a specific USK edition probe.
* <li>Tracks success, DNF, cancellation, and cooldown state.
* <li>Provides scheduling and priority hooks for the polling pipeline.
* </ul>
*/
public final class USKAttempt implements USKCheckerCallback {
/** Logger for attempt scheduling diagnostics. */
private static final Logger LOG = LoggerFactory.getLogger(USKAttempt.class);

/** Literal used in attempt descriptions to keep log formatting consistent. */
private static final String FOR_LITERAL = " for ";

/** Edition number. */
long number;

/** Attempt to fetch that edition number (or null if the fetch has finished). */
USKChecker checker;

/** Successful fetch? */
boolean succeeded;

/** DNF? */
boolean dnf;

/** Whether this attempt has been explicitly canceled. */
boolean cancelled;

/** The lookup descriptor associated with this attempt. */
final USKKeyWatchSet.Lookup lookup;

/** Whether this attempt is a long-lived polling attempt. */
final boolean forever;

/** Whether this attempt has ever entered finite cooldown. */
private boolean everInCooldown;

/** Whether cancellation has already been reported to callbacks. */
private boolean cancelNotified;

/** Callback target for attempt lifecycle events. */
private final USKAttemptCallbacks callbacks;

/** Base USK used for logging and manager lookups. */
private final USK origUSK;

/** Parent requester that supplies priority and scheduling policy. */
private final ClientRequester parent;

/**
* Creates a new attempt for the provided lookup descriptor.
*
* <p>The constructor wires the checker used to probe the target edition and initializes the
* attempt state for scheduling. When {@code forever} is {@code true}, the checker is created for
* a long-lived polling attempt; otherwise it represents a one-off probe that will retire after
* completion.
*
* @param attemptContext shared configuration for attempt construction
* @param lookup descriptor containing edition and key information
* @param forever {@code true} to create a polling attempt; {@code false} for a one-off probe
*/
USKAttempt(USKAttemptContext attemptContext, USKKeyWatchSet.Lookup lookup, boolean forever) {
this.callbacks = attemptContext.callbacks();
this.origUSK = attemptContext.origUSK();
this.parent = attemptContext.parent();
this.lookup = lookup;
this.number = lookup.val;
this.succeeded = false;
this.dnf = false;
this.forever = forever;
this.checker =
new USKChecker(
this,
lookup.key,
forever ? -1 : attemptContext.ctx().maxUSKRetries,
lookup.ignoreStore ? attemptContext.ctxNoStore() : attemptContext.ctx(),
attemptContext.parent(),
attemptContext.realTimeFlag());
}

@Override
public void onDNF(ClientContext context) {
synchronized (this) {
checker = null;
dnf = true;
}
callbacks.onDNF(this, context);
}

@Override
public void onSuccess(ClientSSKBlock block, ClientContext context) {
synchronized (this) {
checker = null;
succeeded = true;
}
callbacks.onSuccess(this, false, block, context);
}

@Override
public void onFatalAuthorError(ClientContext context) {
synchronized (this) {
checker = null;
}
// Counts as success except it doesn't update
callbacks.onSuccess(this, true, null, context);
}

@Override
public void onNetworkError(ClientContext context) {
synchronized (this) {
checker = null;
}
// Treat network error as DNF for scheduling purposes
callbacks.onDNF(this, context);
}

@Override
public void onCancelled(ClientContext context) {
synchronized (this) {
checker = null;
if (cancelNotified) return;
cancelNotified = true;
}
callbacks.onCancelled(this, context);
}

/**
* Cancels this attempt and propagates cancellation to the checker if present.
*
* @param context client context used to cancel scheduling; must not be null
*/
public void cancel(ClientContext context) {
cancelled = true;
USKChecker c;
synchronized (this) {
c = checker;
}
if (c != null) {
c.cancel(context);
}
onCancelled(context);
}

/**
* Schedules this attempt with its checker if still active.
*
* @param context client context used to schedule the checker; must not be null
*/
public void schedule(ClientContext context) {
USKChecker c;
synchronized (this) {
c = checker;
}
if (c == null) {
if (LOG.isDebugEnabled()) LOG.debug("Checker == null in schedule() for {}", this);
} else {
assert (!c.persistent());
c.schedule(context);
}
}

@Override
public String toString() {
return "USKAttempt for "
+ number
+ FOR_LITERAL
+ origUSK.getURI()
+ (forever ? " (forever)" : "");
}

@Override
public short getPriority() {
if (callbacks.isBackgroundPoll()) {
synchronized (this) {
if (forever) {
if (!everInCooldown) {
// Boost the priority initially, so that finding the first edition takes precedence
// over ongoing polling after we're fairly sure we're not going to find anything.
// The ongoing polling keeps the ULPRs up to date so that we will get told quickly,
// but if we are overloaded, we won't be able to keep up regardless.
return callbacks.getProgressPollPriority();
} else {
return callbacks.getNormalPollPriority();
}
} else {
// If !forever, this is a random-probe.
// It's not that important.
return callbacks.getNormalPollPriority();
}
}
}
return parent.getPriorityClass();
}

@Override
public void onEnterFiniteCooldown(ClientContext context) {
synchronized (this) {
everInCooldown = true;
}
callbacks.onEnterFiniteCooldown(context);
}

/**
* Reports whether this attempt has ever entered a finite cooldown.
*
* @return {@code true} if the attempt has cooled down at least once
*/
public synchronized boolean everInCooldown() {
return everInCooldown;
}

/** Refreshes cached poll parameters on the underlying checker, if active. */
public void reloadPollParameters() {
USKChecker c;
synchronized (this) {
c = checker;
}
if (c == null) return;
c.onChangedFetchContext();
}
}
101 changes: 101 additions & 0 deletions src/main/java/network/crypta/client/async/USKAttemptCallbacks.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package network.crypta.client.async;

import java.util.Random;
import network.crypta.keys.ClientSSKBlock;

/**
* Callback interface for {@link USKAttempt} lifecycle events.
*
* <p>Implementations receive completion and scheduling signals from polling attempts. These hooks
* allow the owning fetcher to react to success, DNF, cancellation, and cooldown transitions while
* providing priority information used by the scheduler. The callbacks are intentionally minimal and
* are expected to be fast, as they are invoked on scheduling or network threads.
*
* <p>The interface is stateful in the sense that implementations can depend on the owning fetcher
* state, but callers should treat each method as a synchronous notification. No concurrency
* guarantees are enforced beyond what the caller provides, so implementations should provide their
* own synchronization if they mutate a shared state.
*
* <ul>
* <li>Signals attempt completion and cancellation events.
* <li>Provides polling priority hints for background scheduling.
* <li>Controls whether random editions should be probed in a round.
* </ul>
*/
interface USKAttemptCallbacks {
/**
* Notifies that an attempt resulted in a DNF outcome.
*
* <p>Implementations may record the failure, reschedule work, or update the UI state. The attempt
* is already marked as complete when this callback runs.
*
* @param attempt attempt that reported the DNF result; never null
* @param context client context associated with the attempt; must not be null
*/
void onDNF(USKAttempt attempt, ClientContext context);

/**
* Notifies that an attempt succeeded.
*
* <p>The callback receives the decoded block if available and a flag indicating that the success
* should not update internal edition tracking. Implementations typically decide whether to decode
* or propagate data based on these inputs.
*
* @param attempt attempt that reported success; never null
* @param dontUpdate whether the success should avoid updating edition tracking
* @param block decoded block returned by the attempt; may be null
* @param context client context associated with the attempt; must not be null
*/
void onSuccess(
USKAttempt attempt, boolean dontUpdate, ClientSSKBlock block, ClientContext context);

/**
* Notifies that an attempt was canceled.
*
* <p>This callback is invoked after the attempt has been marked canceled and any checker has been
* shut down.
*
* @param attempt attempt that was canceled; never null
* @param context client context associated with the attempt; must not be null
*/
void onCancelled(USKAttempt attempt, ClientContext context);

/**
* Notifies that an attempt entered a finite cooldown period.
*
* <p>This signal is used to determine when a polling round can be treated as finished for now.
*
* @param context client context associated with the attempt; must not be null
*/
void onEnterFiniteCooldown(ClientContext context);

/**
* Indicates whether the owning fetcher is running background polling.
*
* @return {@code true} when background polling is active
*/
boolean isBackgroundPoll();

/**
* Returns the polling priority used while making progress on a round.
*
* @return priority class for progress-oriented polling
*/
short getProgressPollPriority();

/**
* Returns the polling priority used during steady-state background polling.
*
* @return priority class for normal background polling
*/
short getNormalPollPriority();

/**
* Determines whether random editions should be added during polling.
*
* @param random random source used to sample candidates; must not be null
* @param firstLoop whether the round is in its initial loop
* @return {@code true} to schedule random editions, otherwise {@code false}
*/
boolean shouldAddRandomEditions(Random random, boolean firstLoop);
}
25 changes: 25 additions & 0 deletions src/main/java/network/crypta/client/async/USKAttemptContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package network.crypta.client.async;

import network.crypta.client.FetchContext;
import network.crypta.keys.USK;

/**
* Shared configuration for creating {@link USKAttempt} instances.
*
* <p>This bundles the stable dependencies required to spawn attempt checkers so callers can reuse a
* single parameter object when scheduling multiple attempts.
*
* @param callbacks owning callback handler for lifecycle events
* @param origUSK base USK used for logging
* @param ctx base fetch context for scheduling
* @param ctxNoStore no-store fetch context for probes that bypass the store
* @param parent parent requester providing scheduling policy
* @param realTimeFlag whether to use real-time scheduling for the checker
*/
record USKAttemptContext(
USKAttemptCallbacks callbacks,
USK origUSK,
FetchContext ctx,
FetchContext ctxNoStore,
ClientRequester parent,
boolean realTimeFlag) {}
Loading
Loading