Repository navigation
create: keep ids server-owned - #70
Conversation
9f047aa to
481cc61
Compare
|
I'm confined to a phone screen today, but if you checkout the subproject '.release" in the validate repo, and follow the README steps, it'll create a release PR which I can merge and will auto-release. |
481cc61 to
64a7d8f
Compare
|
validate v1.0.1 is published. Local results: MySQL passed 418/418 in normal and coverage modes. JSON and TOML passed 395/395 each. Prettier and the workspace hook passed. ESLint has no errors. |
|
This is still a draft. Since you're building on it in another PR, perhaps come back and finish it up? |
A zone's `nameservers` is the list of nameserver ids that serve it, as
in NicTool 2: the apex NS set, with the lowest id as the SOA MNAME.
mysql maps it to nt_zone_nameserver; the file stores keep it on the zone
row. Both return it ascending. GET /zone/{id}/ns lists the active ones in
that order, and destroy drops the assignments, since nt_zone_nameserver
has no foreign key to cascade from.
POST /zone without `nameservers` takes the group's usable nameservers,
the default NicTool 2 offered.
Needs @nictool/validate 1.0, where `nameservers` is a list of ids and PUT
accepts it, so this builds on #70.
A permission create loaded the user file, then saved it outside the per-file lock, so users created meanwhile vanished.
authenticate() saved the user list it loaded before hashing, so users created during a login's hash upgrade vanished.
d8c2fef to
63c57d0
Compare
|
The two new fixes look right. Permission create now goes through Copilot's permission-create finding is one case of a wider problem, though. Every const rows = await this._load() // read outside the lock
// …modify rows…
await this._save(rows) // write the stale list back
To reproduce, run this from the package root against import fs from 'node:fs'
process.env.NICTOOL_DATA_STORE = 'json'
process.env.NICTOOL_DATA_STORE_PATH = fs.mkdtempSync('/tmp/store-')
const { default: Zone } = await import('./lib/zone/store/file.js')
const z = new Zone()
const seed = await z.create({ gid: 1, zone: 'seed.example' })
let lost = 0
for (let i = 0; i < 200; i++) {
const [id] = await Promise.all([
z.create({ gid: 1, zone: `n${i}.example` }),
z.put({ id: seed, description: `d${i}` }),
])
if ((await z.get({ id })).length === 0) lost++
}
console.log(`create lost to a concurrent put: ${lost}/200`)It prints Suggested fix: move each load-modify-save into |
put, delete and destroy saved a list they had loaded earlier, so rows created in between vanished. They now change rows inside mutate(), which skips the write when the callback returns false. Nothing writes outside the lock, so save() and the _save helpers are gone. A file without last_id now records its highest id before a change, so destroying that row can't free its id for reuse. A permission with no user or group fails on the file stores instead of storing nothing: inline permission ids are user and group ids, so there is no free id.
|
Done in 4f76654. Your repro now prints |
|
Open items, most serious first. Fix before merge
Lower risk
Cleanup
|
A load during a write could read half a file and fail to parse. _write now writes a copy with the old file's mode and renames it over the file.
- A permission with no user or group answers 400 on every store; over
HTTP mysql answered 500 too, from the response check.
- get({ id }) and a permission put, delete or destroy now look in the
same order: users, standalone, then groups. A null user or group id
counts as absent on both stores.
- nextId skips a row id that isn't an unsigned integer, as mutate()'s
last_id scan does, and one claimId() helper allocates for every store.
- File-store permission creates run one at a time, so two creates with
one explicit id can't both pass the free-id check.
- mutate() hands callbacks keep(), so an existing session or an
ifExists return leaves the file alone.
- Race tests cover put, delete and destroy on every file store.
- The FileStore comment names last_id, and three comments are gone.
|
Answered in ee6b118, plus 33c1483 for item 3.
|
|
Regression from the item 2 fix: get({id}) now falls back to group permissions, so GET and DELETE /permission/{id} can reach a group's permission that has the same id as a user. The comment Abraham deleted warned about exactly this. I confirmed it with a probe on json:
Abraham's reply admits that ids collide across the user, group and standalone files. What's new is that HTTP can now reach the collision. Switching a user back to inheriting is a normal UI action, so this is likely to happen. Possible fixes:
|
User and group permissions share ids in the file stores. With groups in
get({ id }) and _change(), destroying a user's permission left the
group's with the same id reachable, so DELETE /permission/{id} removed
it. Group permissions are reached by gid again.
|
You're right, fixed in fe5b78a, with your repro as a test. |
A zone's `nameservers` is the list of nameserver ids that serve it, as
in NicTool 2: the apex NS set, with the lowest id as the SOA MNAME.
mysql maps it to nt_zone_nameserver; the file stores keep it on the zone
row. Both return it ascending. GET /zone/{id}/ns lists the active ones in
that order, and destroy drops the assignments, since nt_zone_nameserver
has no foreign key to cascade from.
POST /zone without `nameservers` takes the group's usable nameservers,
the default NicTool 2 offered.
Needs @nictool/validate 1.0, where `nameservers` is a list of ids and PUT
accepts it, so this builds on #70.

NicTool/validate#34 forbids
idon create, and validate 1.0.1 ships it, so this bumps the dependency. Every HTTP POST now leaves id allocation to the store. mysql uses its auto-increment columns. The json and toml stores serialize create allocation per file in the API process. A high-water id prevents reuse after the highest row is deleted. Allocation fails when the backend id type is exhausted.Internal creates with explicit ids keep #69's collision rejection. HTTP creates refuse an id for every object: group, nameserver, permission, user, zone and zone record. Creates without an id still allocate one. PUT keeps using the id in the route.
A regression runs twenty concurrent group creates on json and toml. Each gets ids 1 through 20. After id 20 is destroyed, the next create gets 21.
Coverage now measures the json store as well as mysql. It goes through each backend's serial runner, because test files running in parallel rewrote the same store files.
This completes the last open API #6 item, caller-controlled auto-increment fields. Its permissions checkbox is still unticked, but #57 already implemented it.
Every file-store write now runs under that per-file lock: create, put, delete, destroy and the login hash upgrade. Each used to save a list it had loaded earlier, so rows created in between vanished. Regressions force each interleaving on json and toml. A write goes to a temp file renamed over the store file, so an unlocked load never reads half of one. A permission with no user or group answers 400 on every store.