feat: publish NIRO Gen5W research overview and reviewed source examples
This commit is contained in:
commit
ccad0ed9f3
23 files changed
+1279
No files matched your search
@@ -0,0 +1,64 @@
|
||||
package com.chaegeon.niro;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Root-only, per-run E2E evidence. No request, location or secret is stored. */
|
||||
final class WeatherRefreshCounters {
|
||||
private final File directory;
|
||||
private long signal;
|
||||
private long callback;
|
||||
|
||||
WeatherRefreshCounters(File directory) throws IOException {
|
||||
if (!directory.isDirectory()
|
||||
|| !directory.getCanonicalPath().equals(directory.getAbsolutePath())) {
|
||||
throw new IOException("invalid counter directory");
|
||||
}
|
||||
this.directory = directory;
|
||||
writeAtomic("signal.counter", 0L);
|
||||
writeAtomic("callback.counter", 0L);
|
||||
}
|
||||
|
||||
synchronized void signalAccepted() throws IOException {
|
||||
if (signal == Long.MAX_VALUE) {
|
||||
throw new IOException("signal counter overflow");
|
||||
}
|
||||
writeAtomic("signal.counter", signal + 1L);
|
||||
++signal;
|
||||
}
|
||||
|
||||
synchronized void freshCallbackCompleted() throws IOException {
|
||||
if (callback == Long.MAX_VALUE) {
|
||||
throw new IOException("callback counter overflow");
|
||||
}
|
||||
writeAtomic("callback.counter", callback + 1L);
|
||||
++callback;
|
||||
}
|
||||
|
||||
private void writeAtomic(String name, long value) throws IOException {
|
||||
File target = new File(directory, name);
|
||||
File pending = new File(directory, name + ".new");
|
||||
if (pending.exists() || !pending.getCanonicalPath().equals(pending.getAbsolutePath())
|
||||
|| !target.getCanonicalPath().equals(target.getAbsolutePath())) {
|
||||
throw new IOException("counter path not safe");
|
||||
}
|
||||
FileOutputStream output = new FileOutputStream(pending);
|
||||
try {
|
||||
if (!pending.setReadable(false, false)
|
||||
|| !pending.setWritable(false, false)
|
||||
|| !pending.setExecutable(false, false)
|
||||
|| !pending.setReadable(true, true)
|
||||
|| !pending.setWritable(true, true)) {
|
||||
throw new IOException("counter permission failure");
|
||||
}
|
||||
output.write((Long.toString(value) + "\n").getBytes("US-ASCII"));
|
||||
output.getFD().sync();
|
||||
} finally {
|
||||
output.close();
|
||||
}
|
||||
if (!pending.renameTo(target)) {
|
||||
throw new IOException("counter replacement failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user