88 lines
3.1 KiB
Java
88 lines
3.1 KiB
Java
package com.chaegeon.niro;
|
|
|
|
/** Pure-Java admission gate for stock WeatherActivity refresh requests. */
|
|
public final class WeatherRefreshGate {
|
|
public enum Admission {
|
|
FETCH,
|
|
REPLAY,
|
|
COALESCE,
|
|
BACKOFF
|
|
}
|
|
|
|
private final long cooldownMillis;
|
|
private final long failureRetryMillis;
|
|
private final long replaySpacingMillis;
|
|
private boolean running;
|
|
private Admission active;
|
|
private long nextFetchAt;
|
|
private long nextRetryAt;
|
|
private long nextReplayAt;
|
|
|
|
public WeatherRefreshGate(long cooldownMillis, long failureRetryMillis,
|
|
long replaySpacingMillis) {
|
|
if (cooldownMillis < 0L || failureRetryMillis < 0L
|
|
|| replaySpacingMillis < 0L) {
|
|
throw new IllegalArgumentException("invalid gate interval");
|
|
}
|
|
this.cooldownMillis = cooldownMillis;
|
|
this.failureRetryMillis = failureRetryMillis;
|
|
this.replaySpacingMillis = replaySpacingMillis;
|
|
}
|
|
|
|
public synchronized Admission admit(long elapsedMillis) {
|
|
if (running) {
|
|
// The in-flight fetch/replay will broadcast to all active listeners.
|
|
return Admission.COALESCE;
|
|
}
|
|
if (elapsedMillis < nextRetryAt) {
|
|
// A fast-failing privileged sender must not hammer the root shell
|
|
// or online service. There is no successful response to replay.
|
|
return Admission.BACKOFF;
|
|
}
|
|
running = true;
|
|
active = elapsedMillis < nextFetchAt ? Admission.REPLAY : Admission.FETCH;
|
|
return active;
|
|
}
|
|
|
|
public synchronized void finish(long elapsedMillis, Admission admission,
|
|
boolean success) {
|
|
if (!running) {
|
|
throw new IllegalStateException("not running");
|
|
}
|
|
if (admission == Admission.COALESCE || admission == Admission.BACKOFF) {
|
|
throw new IllegalArgumentException("request did not start a worker");
|
|
}
|
|
if (admission != active) {
|
|
throw new IllegalArgumentException("wrong active request");
|
|
}
|
|
running = false;
|
|
active = null;
|
|
if (!success) {
|
|
// Never replay a response after a failed fetch or failed replay.
|
|
nextFetchAt = 0L;
|
|
nextRetryAt = elapsedMillis + failureRetryMillis;
|
|
nextReplayAt = 0L;
|
|
} else if (admission == Admission.FETCH) {
|
|
// Replays do not extend freshness. A new fetch follows 60 s later.
|
|
nextFetchAt = elapsedMillis + cooldownMillis;
|
|
nextRetryAt = 0L;
|
|
nextReplayAt = elapsedMillis + replaySpacingMillis;
|
|
} else {
|
|
nextRetryAt = 0L;
|
|
nextReplayAt = elapsedMillis + replaySpacingMillis;
|
|
}
|
|
}
|
|
|
|
public synchronized boolean isRunning() {
|
|
return running;
|
|
}
|
|
|
|
/** Delay one admitted replay; new arrivals coalesce while its worker waits. */
|
|
public synchronized long replayDelayMillis(long elapsedMillis) {
|
|
if (active != Admission.REPLAY) {
|
|
throw new IllegalStateException("no admitted replay");
|
|
}
|
|
return elapsedMillis < nextReplayAt ? nextReplayAt - elapsedMillis : 0L;
|
|
}
|
|
}
|