Rebuild: the fragment merger
Test and deploy / test (push) Successful in 19s
Test and deploy / deploy (push) Skipped

Files cut by a range's edge are put together once the ranges around them
are done (coordinator:run, every minute): a complete file is judged by the
rules and written as a release chunk with probe candidates; one still
missing parts while both neighbouring ranges are done is dropped and
counted (incomplete/edge). A neighbour not planned yet counts as not done,
except before the group's oldest article. BatchStore.writeReleases is the
shared write path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-09 02:19:53 +02:00
1 parent 2c14315f94
commit b862201991
7 files changed
+365 -65

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import BatchStore from '#services/batch_store'
import FragmentMerger from '#services/fragment_merger'
import { RangeIndexer } from '#lib/edge/index'
import { decodeChunk } from '#lib/chunks/index'
import { DEFAULT_RULES } from '#lib/rules/index'
import type { Overview } from '#lib/nntp/index'
const GROUP = 'alt.binaries.test'
const rules = { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } }
function parts(name: string, total: number, numbers: number[], at: number): Overview[] {
return numbers.map((n, i) => [at + i, {
'subject': `Big.Upload.2020 [1/1] - "${name}" yEnc (${n}/${total})`, 'from': 'p <p@x>', 'date': 'Mon, 05 Oct 2026 10:00:00 +0000',
'message-id': `<${name}-${n}@test>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at + i}`,
}] as Overview)
}
/** Indexes one range's overviews and stores them as the range's only batch; the range ends done. */
async function indexRange(groupId: number, nodeId: number, start: number, end: number, overviews: Overview[]) {
const [r] = await db.table('ranges').insert({ group_id: groupId, start_id: start, end_id: end, next_id: start, status: 'leased', node_id: nodeId }).returning('id')
const indexer = new RangeIndexer({ group: GROUP, start, end, rules, edgeArticles: 100 })
indexer.add(overviews, end + 1)
indexer.finish()
await BatchStore.store(Number(r.id), nodeId, indexer.take())
await db.from('ranges').where('id', r.id).update({ status: 'done' })
return Number(r.id)
}
test.group('FragmentMerger', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('a file cut by a range edge is completed from both sides once both ranges are done', async ({ assert }) => {
const [g] = await db.table('groups').insert({ name: GROUP, priority: 1, low: 1 }).returning('id')
const [n] = await db.table('nodes').insert({ name: 'merge-node' }).returning('id')
// Parts 1–2 at the end of the first range (still open there), 3–4 at the start of the next.
await indexRange(g.id, n.id, 1, 1000, parts('big.part1.rar', 4, [1, 2], 990))
assert.equal((await db.from('fragments').count('* as c'))[0].c, '1')
assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 0, waiting: 1 })
await indexRange(g.id, n.id, 1001, 2000, parts('big.part1.rar', 4, [3, 4], 1001))
assert.include(await FragmentMerger.run(), { completed: 1, incomplete: 0 })
assert.equal((await db.from('fragments').count('* as c'))[0].c, '0')
const release = await db.from('releases').first()
assert.include(release, { files: 1, chunks: 1 })
const chunk = await db.from('release_chunks').where('release_id', release.id).first()
assert.deepEqual(decodeChunk(chunk.payload)[0].segments.map((s) => s[0]), [1, 2, 3, 4])
assert.deepEqual((await db.from('probe_candidates')).map((c) => c.kind), ['archive'])
})
test('a file still missing parts when all its neighbours are done is dropped', async ({ assert }) => {
const [g] = await db.table('groups').insert({ name: GROUP, priority: 1, low: 1 }).returning('id')
const [n] = await db.table('nodes').insert({ name: 'merge-node' }).returning('id')
await indexRange(g.id, n.id, 1, 1000, parts('gap.part1.rar', 5, [1, 2], 990))
await indexRange(g.id, n.id, 1001, 2000, parts('gap.part1.rar', 5, [4, 5], 1001))
// Its later neighbour isn't there yet: the missing part could still come.
assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 0, waiting: 1 })
await indexRange(g.id, n.id, 2001, 3000, [])
assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 1 })
assert.equal((await db.from('fragments').count('* as c'))[0].c, '0')
assert.lengthOf(await db.from('releases'), 0)
})
})