Skip to content
Closed
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
36 changes: 35 additions & 1 deletion src/arweave/composite-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { strict as assert } from 'node:assert';
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import { default as Arweave } from 'arweave';

import { ArweaveCompositeClient } from './composite-client.js';
import {
ArweaveCompositeClient,
chunkPostPeersCacheKey,
} from './composite-client.js';
import { UniformFailureSimulator } from '../lib/chaos.js';
import { ArweavePeerManager } from '../peers/arweave-peer-manager.js';
import log from '../log.js';
Expand Down Expand Up @@ -409,4 +412,35 @@ describe('ArweaveCompositeClient', () => {
(client as any).peerGetChunk = originalPeerGetChunk;
});
});

describe('chunkPostPeersCacheKey', () => {
it('distinguishes different peer sets of equal length', () => {
// The bug this fixes: a length-only key collided these onto one cached
// ordering for the full cache window, narrowing seeding diversity.
const a = ['http://a:1984', 'http://b:1984', 'http://c:1984'];
const b = ['http://x:1984', 'http://y:1984', 'http://z:1984'];
assert.equal(a.length, b.length);
assert.notEqual(chunkPostPeersCacheKey(a), chunkPostPeersCacheKey(b));
});

it('is order-independent for the same set (set identity)', () => {
const ordered = ['http://a:1984', 'http://b:1984', 'http://c:1984'];
const shuffled = ['http://c:1984', 'http://a:1984', 'http://b:1984'];
assert.equal(
chunkPostPeersCacheKey(ordered),
chunkPostPeersCacheKey(shuffled),
);
});

it('does not mutate the input array', () => {
const peers = ['http://c:1984', 'http://a:1984', 'http://b:1984'];
const snapshot = [...peers];
chunkPostPeersCacheKey(peers);
assert.deepEqual(peers, snapshot);
});

it('handles the empty set', () => {
assert.equal(chunkPostPeersCacheKey([]), '');
});
});
});
26 changes: 21 additions & 5 deletions src/arweave/composite-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@ interface PeerChunkQueue {
totalSuccesses: number;
}

/**
* Stable memo key for the chunk-POST peer-sort cache. Identifies the *set* of
* eligible peers independent of order, so two different peer sets of equal
* length no longer collide on a shared cached ordering (the prior length-only
* key did, narrowing seeding diversity within the cache window).
*
* Cheap by design: one sort + join over the bounded postChunk peer set, far
* less work than the weighted selection the memo guards. Does not mutate the
* caller's array (`slice` before `sort`).
*/
export function chunkPostPeersCacheKey(peers: readonly string[]): string {
return peers.slice().sort().join(',');
}

export class ArweaveCompositeClient
implements
ChainSource,
Expand Down Expand Up @@ -311,11 +325,13 @@ export class ArweaveCompositeClient
},
{
maxAge: config.CHUNK_POST_SORTED_PEERS_CACHE_DURATION_MS,
// Use array length as cache key for O(1) performance. This means different
// peer lists of the same length will share cached results, which is acceptable
// because: 1) peer weights change gradually, 2) the cache duration is short (10s),
// and 3) this avoids expensive operations on every chunk POST request.
normalizer: (args) => args[0].length.toString(),
// Key by the *set* of eligible peers (order-independent), not its length.
// The prior length-only key collided distinct peer sets of equal size onto
// one cached ordering for the full 10s window, quietly narrowing seeding
// diversity. This key is still cheap — a sort+join over the bounded
// postChunk peer set, far less than the weighted selection it guards — and
// the 10s maxAge still absorbs gradual weight drift.
normalizer: (args) => chunkPostPeersCacheKey(args[0]),
},
);

Expand Down
Loading