Skip to content
Merged
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
2 changes: 1 addition & 1 deletion documents/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ modules/checkpoints/
- A read writes its requests to `cat-file` while it takes the answers, since `cat-file` stops reading once its output is full.
- `cat-file` runs with `--buffer`, and each read's requests end with `flush`, so it writes its answers in full buffers, as do the one-shot `cat-file --batch` runs that list checkpoints and stage a restore's files, which end at the end of their input; without it, each answer is a pipe write of its own.
- The module names what it reads by object id; a `<reference>:<path>` name makes Git search the commit's trees again for each one, so ten thousand names in one folder of ten thousand files took 40 seconds where their ids took under half a second, and only `checkpoint.json` is read by its path.
- Writing to a command waits while it reads more slowly than the module writes; a command that ends early closes its input, what is written after that is dropped, and its exit says what went wrong.
- Writing to a command waits while it reads more slowly than the module writes; a command that ends early closes its input, which fails a write as a broken pipe or, on macOS, at times as no longer connected; what is written after that is dropped, and its exit says what went wrong.
- `cat-file`'s output is read as it comes, in parts cut anywhere: a name with no object answers `<name> missing`, and that name may hold a space; output that ends inside a content gives no object for it; and a name `cat-file --batch` gave no answer for reads as missing.
- `cat-file` finds what a capture wrote after it started, since Git looks for new packs when an object is missing and reads each reference as it's asked for.
- A store's commands end once neither has been used for 10 minutes, before its housekeeping ([Keeping and deleting](#keeping-and-deleting)), before its folder is deleted, after a request cancelled while it ran, and as the runtime part deactivates: `cat-file` at the end of its input, `fast-import` at its `done`, each stopped if it still runs 10 seconds later.
Expand Down
2 changes: 1 addition & 1 deletion src/noldova.checkpoints/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class Resources {
public static readonly checkpointReferencePrefix: string = "refs/checkpoints/";
public static readonly checkpointTree: string = "files/ tree";
public static readonly cleaningRecord: string = "Cleaning";
public static readonly closedInputCodes: readonly string[] = ["EPIPE", "EOF", "ERR_STREAM_DESTROYED", "ERR_STREAM_WRITE_AFTER_END"];
public static readonly closedInputCodes: readonly string[] = ["ENOTCONN", "EOF", "EPIPE", "ERR_STREAM_DESTROYED", "ERR_STREAM_WRITE_AFTER_END"];
public static readonly commitAuthor: string = "Checkpoints <checkpoints@noldova.invalid>";
public static readonly commitMark: string = ":1";
public static readonly configFile: string = "config";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ export class CheckpointsHarness {
const git = await this.processes.startAsync(new ProcessRequest(this.build.program, args, this.root, environment));
const refused: Error[] = [];
git.input.on("error", (error: Error) => {
if (!FailureFixture.hasCode(error, "EPIPE"))
if (!FailureFixture.hasCode(error, "EPIPE") && !FailureFixture.hasCode(error, "ENOTCONN"))
refused.push(error);
});
git.input.end(input);
Expand Down
23 changes: 21 additions & 2 deletions src/noldova.checkpoints/tests/services/input-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { describe, it } from "node:test";
import { InputWriter } from "../../src/services/input-writer.ts";

class PipeError extends Error {
public readonly code: string = "EPIPE";
public readonly code: string;

public constructor(code: string) {
super(`write ${code}`);

this.code = code;
}
}

describe("InputWriter", { concurrency: true }, () => {
Expand Down Expand Up @@ -58,7 +64,20 @@ describe("InputWriter", { concurrency: true }, () => {
const writer = new InputWriter(stream);

const writing = writer.writeAsync("more than four bytes");
stream.destroy(new PipeError("write EPIPE"));
stream.destroy(new PipeError("EPIPE"));
await writing;
await writer.writeAsync("after");
writer.end();

assert.equal(stream.destroyed, true);
});

it("stops waiting when the program ends early and macOS reports its input as no longer connected, and drops what is written after", async () => {
const stream = new PassThrough({ highWaterMark: 4 });
const writer = new InputWriter(stream);

const writing = writer.writeAsync("more than four bytes");
stream.destroy(new PipeError("ENOTCONN"));
await writing;
await writer.writeAsync("after");
writer.end();
Expand Down
Loading