Skip to content

Commit d087c8e

Browse files
committed
add more btrfs tests
1 parent 92c74be commit d087c8e

File tree

7 files changed

+284
-28
lines changed

7 files changed

+284
-28
lines changed

src/packages/file-server/btrfs/subvolume.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,17 @@ A subvolume
44

55
import { type Filesystem, DEFAULT_SUBVOLUME_SIZE } from "./filesystem";
66
import refCache from "@cocalc/util/refcache";
7-
import {
8-
exists,
9-
listdir,
10-
mkdirp,
11-
sudo,
12-
} from "./util";
7+
import { exists, listdir, mkdirp, sudo } from "./util";
138
import { join } from "path";
149
import { updateRollingSnapshots, type SnapshotCounts } from "./snapshots";
15-
import { human_readable_size } from "@cocalc/util/misc";
10+
//import { human_readable_size } from "@cocalc/util/misc";
11+
import getLogger from "@cocalc/backend/logger";
1612

1713
export const SNAPSHOTS = ".snapshots";
1814
const SEND_SNAPSHOT_PREFIX = "send-";
19-
2015
const BUP_SNAPSHOT = "temp-bup-snapshot";
21-
2216
const PAD = 4;
2317

24-
import getLogger from "@cocalc/backend/logger";
25-
2618
const logger = getLogger("file-server:storage-btrfs:subvolume");
2719

2820
interface Options {
@@ -88,6 +80,20 @@ export class Subvolume {
8880
return x["qgroup-show"][0];
8981
};
9082

83+
quota = async (): Promise<{
84+
size: number;
85+
used: number;
86+
}> => {
87+
let { max_referenced: size, referenced: used } = await this.quotaInfo();
88+
if (size == "none") {
89+
size = null;
90+
}
91+
return {
92+
used,
93+
size,
94+
};
95+
};
96+
9197
size = async (size: string | number) => {
9298
if (!size) {
9399
throw Error("size must be specified");
@@ -98,23 +104,50 @@ export class Subvolume {
98104
});
99105
};
100106

107+
du = async () => {
108+
return await sudo({
109+
command: "btrfs",
110+
args: ["filesystem", "du", "-s", this.path],
111+
});
112+
};
113+
101114
usage = async (): Promise<{
115+
// used and free in bytes
116+
used: number;
117+
free: number;
102118
size: number;
103-
usage: number;
104-
human: { size: string; usage: string };
105119
}> => {
106-
let { max_referenced: size, referenced: usage } = await this.quotaInfo();
107-
if (size == "none") {
108-
size = null;
120+
const { stdout } = await sudo({
121+
command: "btrfs",
122+
args: ["filesystem", "usage", "-b", this.path],
123+
});
124+
let used: number = -1;
125+
let free: number = -1;
126+
let size: number = -1;
127+
for (const x of stdout.split("\n")) {
128+
if (used == -1) {
129+
const i = x.indexOf("Used:");
130+
if (i != -1) {
131+
used = parseInt(x.split(":")[1].trim());
132+
continue;
133+
}
134+
}
135+
if (free == -1) {
136+
const i = x.indexOf("Free (statfs, df):");
137+
if (i != -1) {
138+
free = parseInt(x.split(":")[1].trim());
139+
continue;
140+
}
141+
}
142+
if (size == -1) {
143+
const i = x.indexOf("Device size:");
144+
if (i != -1) {
145+
size = parseInt(x.split(":")[1].trim());
146+
continue;
147+
}
148+
}
109149
}
110-
return {
111-
usage,
112-
size,
113-
human: {
114-
usage: human_readable_size(usage),
115-
size: size != null ? human_readable_size(size) : size,
116-
},
117-
};
150+
return { used, free, size };
118151
};
119152

120153
private makeSnapshotsDir = async () => {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { before, after, fs } from "./setup";
2+
import { writeFile } from "fs/promises";
3+
import { join } from "path";
4+
5+
beforeAll(before);
6+
7+
//const log = console.log;
8+
const log = (..._args) => {};
9+
10+
describe("stress operations with subvolumes", () => {
11+
const count1 = 10;
12+
it(`create ${count1} subvolumes in serial`, async () => {
13+
const t = Date.now();
14+
for (let i = 0; i < count1; i++) {
15+
await fs.subvolume(`${i}`);
16+
}
17+
log(
18+
`created ${Math.round((count1 / (Date.now() - t)) * 1000)} subvolumes per second serial`,
19+
);
20+
});
21+
22+
it("list them and confirm", async () => {
23+
const v = await fs.list();
24+
expect(v.length).toBe(count1);
25+
});
26+
27+
let count2 = 10;
28+
it(`create ${count2} subvolumes in parallel`, async () => {
29+
const v: any[] = [];
30+
const t = Date.now();
31+
for (let i = 0; i < count2; i++) {
32+
v.push(fs.subvolume(`p-${i}`));
33+
}
34+
await Promise.all(v);
35+
log(
36+
`created ${Math.round((count2 / (Date.now() - t)) * 1000)} subvolumes per second in parallel`,
37+
);
38+
});
39+
40+
it("list them and confirm", async () => {
41+
const v = await fs.list();
42+
expect(v.length).toBe(count1 + count2);
43+
});
44+
45+
it("write a file to each volume", async () => {
46+
for (const name of await fs.list()) {
47+
const vol = await fs.subvolume(name);
48+
await writeFile(join(vol.path, "a.txt"), "hi");
49+
}
50+
});
51+
52+
it("clone the first group in serial", async () => {
53+
const t = Date.now();
54+
for (let i = 0; i < count1; i++) {
55+
await fs.cloneSubvolume(`${i}`, `clone-of-${i}`);
56+
}
57+
log(
58+
`cloned ${Math.round((count1 / (Date.now() - t)) * 1000)} subvolumes per second serial`,
59+
);
60+
});
61+
62+
it("clone the second group in parallel", async () => {
63+
const t = Date.now();
64+
const v: any[] = [];
65+
for (let i = 0; i < count2; i++) {
66+
v.push(fs.cloneSubvolume(`p-${i}`, `clone-of-p-${i}`));
67+
}
68+
await Promise.all(v);
69+
log(
70+
`cloned ${Math.round((count2 / (Date.now() - t)) * 1000)} subvolumes per second parallel`,
71+
);
72+
});
73+
74+
it("delete the first batch serial", async () => {
75+
const t = Date.now();
76+
for (let i = 0; i < count1; i++) {
77+
await fs.deleteSubvolume(`${i}`);
78+
}
79+
log(
80+
`deleted ${Math.round((count1 / (Date.now() - t)) * 1000)} subvolumes per second serial`,
81+
);
82+
});
83+
84+
it("delete the second batch in parallel", async () => {
85+
const v: any[] = [];
86+
const t = Date.now();
87+
for (let i = 0; i < count2; i++) {
88+
v.push(fs.deleteSubvolume(`p-${i}`));
89+
}
90+
await Promise.all(v);
91+
log(
92+
`deleted ${Math.round((count2 / (Date.now() - t)) * 1000)} subvolumes per second in parallel`,
93+
);
94+
});
95+
});
96+
97+
afterAll(after);

src/packages/file-server/btrfs/test/setup.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import process from "node:process";
66
import { chmod, mkdtemp, mkdir, rm } from "node:fs/promises";
77
import { tmpdir } from "node:os";
88
import { join } from "path";
9+
import { until } from "@cocalc/util/async-utils";
910

1011
export let fs: Filesystem;
1112
let tempDir;
@@ -26,8 +27,13 @@ export async function before() {
2627
}
2728

2829
export async function after() {
29-
try {
30-
await fs.unmount();
31-
} catch {}
30+
await until(async () => {
31+
try {
32+
await fs.unmount();
33+
return true;
34+
} catch {
35+
return false;
36+
}
37+
});
3238
await rm(tempDir, { force: true, recursive: true });
3339
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { before, after, fs } from "./setup";
2+
import { writeFile } from "fs/promises";
3+
import { join } from "path";
4+
import { delay } from "awaiting";
5+
import { wait } from "@cocalc/backend/conat/test/util";
6+
import { randomBytes } from "crypto";
7+
8+
beforeAll(before);
9+
10+
jest.setTimeout(20000);
11+
describe("setting and getting quota of a subvolume", () => {
12+
let vol;
13+
it("set the quota of a subvolume to 5 M", async () => {
14+
vol = await fs.subvolume("q");
15+
await vol.size("5M");
16+
17+
const { size, used } = await vol.quota();
18+
expect(size).toBe(5 * 1024 * 1024);
19+
expect(used).toBe(0);
20+
});
21+
22+
it("write a file and check usage goes up", async () => {
23+
const buf = randomBytes(4 * 1024 * 1024);
24+
await writeFile(join(vol.path, "buf"), buf);
25+
await wait({
26+
until: async () => {
27+
await delay(1000);
28+
const { used } = await vol.usage();
29+
return used > 0;
30+
},
31+
});
32+
const { used } = await vol.usage();
33+
expect(used).toBeGreaterThan(0);
34+
});
35+
36+
it("fail to write a 50MB file (due to quota)", async () => {
37+
const buf2 = randomBytes(50 * 1024 * 1024);
38+
const b = join(vol.path, "buf2");
39+
expect(async () => {
40+
await writeFile(b, buf2);
41+
}).rejects.toThrow("write");
42+
});
43+
});
44+
45+
afterAll(after);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
type ExecuteCodeOptions,
3+
type ExecuteCodeOutput,
4+
} from "@cocalc/util/types/execute-code";
5+
import { executeCode } from "@cocalc/backend/execute-code";
6+
import getLogger from "@cocalc/backend/logger";
7+
8+
const logger = getLogger("file-server:storage:util");
9+
10+
const DEFAULT_EXEC_TIMEOUT_MS = 60 * 1000;
11+
12+
export async function exists(path: string) {
13+
try {
14+
await sudo({ command: "ls", args: [path], verbose: false });
15+
return true;
16+
} catch {
17+
return false;
18+
}
19+
}
20+
21+
export async function mkdirp(paths: string[]) {
22+
if (paths.length == 0) return;
23+
await sudo({ command: "mkdir", args: ["-p", ...paths] });
24+
}
25+
26+
export async function chmod(args: string[]) {
27+
await sudo({ command: "chmod", args: args });
28+
}
29+
30+
export async function sudo(
31+
opts: ExecuteCodeOptions & { desc?: string },
32+
): Promise<ExecuteCodeOutput> {
33+
if (opts.verbose !== false && opts.desc) {
34+
logger.debug("exec", opts.desc);
35+
}
36+
let command, args;
37+
if (opts.bash) {
38+
command = `sudo ${opts.command}`;
39+
args = undefined;
40+
} else {
41+
command = "sudo";
42+
args = [opts.command, ...(opts.args ?? [])];
43+
}
44+
return await executeCode({
45+
verbose: true,
46+
timeout: DEFAULT_EXEC_TIMEOUT_MS / 1000,
47+
...opts,
48+
command,
49+
args,
50+
});
51+
}
52+
53+
export async function rm(paths: string[]) {
54+
if (paths.length == 0) return;
55+
await sudo({ command: "rm", args: paths });
56+
}
57+
58+
export async function rmdir(paths: string[]) {
59+
if (paths.length == 0) return;
60+
await sudo({ command: "rmdir", args: paths });
61+
}
62+
63+
export async function listdir(path: string) {
64+
const { stdout } = await sudo({ command: "ls", args: ["-1", path] });
65+
return stdout.split("\n").filter((x) => x);
66+
}
67+
68+
export async function isdir(path: string) {
69+
const { stdout } = await sudo({ command: "stat", args: ["-c", "%F", path] });
70+
return stdout.trim() == "directory";
71+
}

src/packages/file-server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"dependencies": {
3030
"@cocalc/backend": "workspace:*",
3131
"@cocalc/file-server": "workspace:*",
32-
"@cocalc/util": "workspace:*"
32+
"@cocalc/util": "workspace:*",
33+
"awaiting": "^3.0.0"
3334
},
3435
"devDependencies": {
3536
"@types/jest": "^29.5.14",

src/packages/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)