Initial Commit
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* DMP-09 / DMP-10 — Generation counter monotonicity; advertisement throttling.
|
||||
*
|
||||
* `GenerationTracker` keeps the highest generation per `(peerId, fileId)`
|
||||
* and drops stale advertisements; it also accepts a one-cycle wrap to
|
||||
* tolerate a single anomaly past 2^31. `AdvertiseThrottle` limits
|
||||
* outgoing advertisements to one per `(neighbor, fileId)` per `windowMs`.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
ADVERTISE_THROTTLE_MS_DEFAULT,
|
||||
ADVERTISE_THROTTLE_MS_MAX,
|
||||
AdvertiseThrottle,
|
||||
DMP_CODES,
|
||||
DmpError,
|
||||
GENERATION_WRAP_THRESHOLD,
|
||||
GenerationTracker,
|
||||
} from "../src/index.js";
|
||||
|
||||
const PEER = "peer-A";
|
||||
const FILE = "F".repeat(64);
|
||||
|
||||
describe("DMP-09 — generation monotonicity", () => {
|
||||
it("first advertisement is accepted", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
const r = t.observe(PEER, FILE, 7);
|
||||
expect(r.accept).toBe(true);
|
||||
expect(r.latest).toBe(7);
|
||||
});
|
||||
|
||||
it("higher generation is accepted", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
t.observe(PEER, FILE, 7);
|
||||
const r = t.observe(PEER, FILE, 8);
|
||||
expect(r.accept).toBe(true);
|
||||
expect(r.latest).toBe(8);
|
||||
});
|
||||
|
||||
it("out-of-order advertisement is dropped", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
t.observe(PEER, FILE, 10);
|
||||
const r = t.observe(PEER, FILE, 5);
|
||||
expect(r.accept).toBe(false);
|
||||
expect(r.reason).toBe("stale");
|
||||
expect(t.peek(PEER, FILE)).toBe(10);
|
||||
});
|
||||
|
||||
it("duplicate generation is dropped", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
t.observe(PEER, FILE, 4);
|
||||
const r = t.observe(PEER, FILE, 4);
|
||||
expect(r.accept).toBe(false);
|
||||
expect(r.reason).toBe("duplicate");
|
||||
});
|
||||
|
||||
it("wrap detected past 2^31 delta", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
const high = 0xff_ff_ff_ff;
|
||||
t.observe(PEER, FILE, high);
|
||||
const r = t.observe(PEER, FILE, 0);
|
||||
expect(r.accept).toBe(true);
|
||||
expect(r.latest).toBe(0);
|
||||
expect(GENERATION_WRAP_THRESHOLD).toBe(2 ** 31);
|
||||
});
|
||||
|
||||
it("negative or non-integer generation rejected", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
expect(t.observe(PEER, FILE, -1).accept).toBe(false);
|
||||
expect(t.observe(PEER, FILE, 1.5).accept).toBe(false);
|
||||
});
|
||||
|
||||
it("dropPeer wipes that peer's entries", { timeout: 5000 }, () => {
|
||||
const t = new GenerationTracker();
|
||||
t.observe(PEER, FILE, 5);
|
||||
t.observe("other", FILE, 9);
|
||||
t.dropPeer(PEER);
|
||||
expect(t.peek(PEER, FILE)).toBeUndefined();
|
||||
expect(t.peek("other", FILE)).toBe(9);
|
||||
});
|
||||
|
||||
it("dropFile wipes the file's entries across peers", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const t = new GenerationTracker();
|
||||
t.observe(PEER, FILE, 5);
|
||||
t.observe(PEER, "other", 9);
|
||||
t.dropFile(FILE);
|
||||
expect(t.peek(PEER, FILE)).toBeUndefined();
|
||||
expect(t.peek(PEER, "other")).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DMP-10 — advertisement throttle", () => {
|
||||
it("first call is allowed", { timeout: 5000 }, () => {
|
||||
const now = 0;
|
||||
const throttle = new AdvertiseThrottle({
|
||||
clock: () => now,
|
||||
windowMs: ADVERTISE_THROTTLE_MS_DEFAULT,
|
||||
});
|
||||
const r = throttle.allow("neighbor-1", FILE);
|
||||
expect(r.allow).toBe(true);
|
||||
expect(r.retryAfterMs).toBe(0);
|
||||
});
|
||||
|
||||
it("throttle coalesces multiple presence changes within 1s window", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
let now = 0;
|
||||
const throttle = new AdvertiseThrottle({
|
||||
clock: () => now,
|
||||
windowMs: 1_000,
|
||||
});
|
||||
// First emission allowed.
|
||||
expect(throttle.allow("n", FILE).allow).toBe(true);
|
||||
now = 200;
|
||||
// Within the window — suppressed and pending.
|
||||
const r2 = throttle.allow("n", FILE);
|
||||
expect(r2.allow).toBe(false);
|
||||
expect(r2.retryAfterMs).toBeGreaterThan(0);
|
||||
expect(throttle.hasPending("n", FILE)).toBe(true);
|
||||
now = 500;
|
||||
// Still in window.
|
||||
expect(throttle.allow("n", FILE).allow).toBe(false);
|
||||
now = 1_001;
|
||||
// Window has elapsed; allowed again.
|
||||
const r4 = throttle.allow("n", FILE);
|
||||
expect(r4.allow).toBe(true);
|
||||
expect(throttle.hasPending("n", FILE)).toBe(false);
|
||||
});
|
||||
|
||||
it("clearPending resets the pending bit", { timeout: 5000 }, () => {
|
||||
let now = 0;
|
||||
const t = new AdvertiseThrottle({
|
||||
clock: () => now,
|
||||
windowMs: 1_000,
|
||||
});
|
||||
t.allow("n", FILE);
|
||||
now = 100;
|
||||
t.allow("n", FILE);
|
||||
expect(t.hasPending("n", FILE)).toBe(true);
|
||||
t.clearPending("n", FILE);
|
||||
expect(t.hasPending("n", FILE)).toBe(false);
|
||||
});
|
||||
|
||||
it("dropNeighbor evicts only that neighbor's state", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const t = new AdvertiseThrottle({
|
||||
clock: () => 0,
|
||||
windowMs: 1_000,
|
||||
});
|
||||
t.allow("a", FILE);
|
||||
t.allow("b", FILE);
|
||||
t.dropNeighbor("a");
|
||||
expect(t.size).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects windowMs ≤ 0 or > 60s with THROTTLE_EXCEEDED", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
new AdvertiseThrottle({ clock: () => 0, windowMs: 0 });
|
||||
throw new Error("expected rejection");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DmpError);
|
||||
expect((err as DmpError).code).toBe(DMP_CODES.THROTTLE_EXCEEDED);
|
||||
}
|
||||
try {
|
||||
new AdvertiseThrottle({
|
||||
clock: () => 0,
|
||||
windowMs: ADVERTISE_THROTTLE_MS_MAX + 1,
|
||||
});
|
||||
throw new Error("expected rejection");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(DmpError);
|
||||
expect((err as DmpError).code).toBe(DMP_CODES.THROTTLE_EXCEEDED);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user