scorpiofs 0.2.2

FUSE-based virtual filesystem with Antares overlay for monorepo builds
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
# Antares HTTP API 文档

## 概述

Antares 是一个轻量级的控制平面,按需创建基于 overlay 语义的 **FUSE(用户态)挂载**。

> 重要说明:这里的 overlay 指的是 `libfuse-fs` 提供的用户态 `OverlayFs`(unionfs/overlay 语义),**不是 Linux 内核 overlayfs**`mount -t overlay ... lowerdir=`)相关的内核态实现。

每个 HTTP 请求会构建一个由以下层组成的 overlay 文件系统:
- **Dicfuse** (共享,只读) 位于底层
- **CL passthrough** (可选,每个挂载独立) 位于中间层
- **Upper passthrough** (每个挂载独立,读写) 位于顶层

### 工作流程
1. 客户端调用 Antares HTTP API,指定 monorepo 路径、可选 CL,以及可选的 `job_id/build_id`
2. 服务器复用共享的 `Dicfuse`(并按 `base_path` 缓存复用),自动生成各挂载的目录,并组装层列表
3. `OverlayFs` 被包装在 `LoggingFileSystem` 中,通过 `rfuse3` 在 Tokio 任务中挂载
4. 响应返回 mount ID;后续的 DELETE 请求可以通过 `mount_id` 或通过 `job_id`(任务粒度)卸载

### Dicfuse 就绪与懒加载
- Antares 创建挂载时只需要 Dicfuse **root inode 已初始化**- 目录元数据采用 **按目录粒度懒加载**:首次 `lookup/readdir` 命中未加载目录时,会拉取该目录的一层 children 并建立 inode/path 映射;文件内容仍保持 read() 时按需拉取。
- 为避免并发下重复加载同一目录,Dicfuse 对每个目录路径有独立锁(同目录只会有一个加载协程在跑)。

### FUSE 层次结构
```mermaid
flowchart TD
    client[HTTP Client] --> api[Antares HTTP API]
    api --> fuse[Antares Mount Orchestrator]
    fuse --> overlay[OverlayFs Builder]
    overlay --> upper["Passthrough Upper (RW)"]
    overlay --> cl["Passthrough CL (RW, optional)"]
    overlay --> dicfuse["Dicfuse (RO, shared)"]
    dicfuse --> storage[(Metadata + Content Store)]
```

---

## API 端点

### 1. 健康检查

**端点**: `GET /health`

**描述**: 返回服务健康状态和运行信息。

**响应** (200 OK):
```json
{
  "status": "healthy",
  "mount_count": 3,
  "uptime_secs": 86400
}
```

**字段说明**:
- `status`: 服务健康状态,`"healthy"``"degraded"`
- `mount_count`: 当前活跃的挂载数量
- `uptime_secs`: 服务运行时间(秒)

---

### 2. 创建挂载

**端点**: `POST /mounts`

**描述**: 创建一个新的 FUSE 挂载。系统会自动基于 UUID 生成所有内部路径。

如果提供 `job_id`/`build_id`,则该 API 对同一任务 ID **幂等**:重复调用会返回同一个挂载(不会创建重复实例),并用于绑定任务生命周期。

**请求体**:
```json
{
  "job_id": "job-123",
  "build_id": "build-456",
  "path": "/third-party/mega",
  "cl": "12345"
}
```

**字段说明**:
- `job_id` (可选): 构建任务标识符(推荐)。提供后创建挂载对该任务幂等。
- `build_id` (可选): 备选的任务标识符。若同时提供 `job_id``build_id`,以 `job_id` 为准。
- `path` (必填): 要挂载的 monorepo 路径
- `cl` (可选): CL (changelist) 标识符,用于创建 CL 层

**幂等与冲突策略**:
- **提供 `job_id/build_id`**  - 同一个 `job_id` 重复 `POST /mounts`:返回同一个挂载(幂等)
  - 同一个 `job_id``path/cl` 不一致:返回 400(防止任务 ID 被误复用)
  - 不同 `job_id` 可以挂载相同的 `path/cl`(支持高并发构建,upper/CL 独立)
- **未提供 `job_id/build_id`**  - 仍沿用旧逻辑:相同 `(path, cl)` 会被判定为重复挂载并返回 400

**路径生成规则**:
所有内部路径基于 UUID 自动生成:
- `mountpoint`: `{antares_mount_root}/{uuid}`
- `upper_dir`: `{antares_upper_root}/{uuid}`
- `cl_dir`: `{antares_cl_root}/{uuid}` (仅在提供 `cl` 时创建)

**响应** (200 OK):
```json
{
  "mount_id": "550e8400-e29b-41d4-a716-446655440000",
  "mountpoint": "/var/lib/antares/mounts/550e8400-e29b-41d4-a716-446655440000"
}
```

**错误响应** (400 Bad Request):
```json
{
  "error": "path cannot be empty",
  "code": "INVALID_REQUEST"
}
```

**错误响应** (400 Bad Request - 重复挂载):
```json
{
  "error": "path /third-party/mega with cl Some(\"12345\") is already mounted",
  "code": "INVALID_REQUEST"
}
```

---

### 3. 列出所有挂载

**端点**: `GET /mounts`

**描述**: 列出所有当前活跃的挂载。

**响应** (200 OK):
```json
{
  "mounts": [
    {
      "mount_id": "550e8400-e29b-41d4-a716-446655440000",
      "job_id": "job-123",
      "path": "/third-party/mega",
      "cl": "12345",
      "mountpoint": "/var/lib/antares/mounts/550e8400-e29b-41d4-a716-446655440000",
      "layers": {
        "upper": "/var/lib/antares/upper/550e8400-e29b-41d4-a716-446655440000",
        "cl": "/var/lib/antares/cl/550e8400-e29b-41d4-a716-446655440000",
        "dicfuse": "shared"
      },
      "state": "Mounted",
      "created_at_epoch_ms": 1702800000000,
      "last_seen_epoch_ms": 1702800123456
    }
  ]
}
```

---

### 4. 查询挂载详情

**端点**: `GET /mounts/{mount_id}`

**描述**: 获取指定挂载的详细信息。

**路径参数**:
- `mount_id`: 挂载的 UUID

**响应** (200 OK):
```json
{
  "mount_id": "550e8400-e29b-41d4-a716-446655440000",
  "job_id": "job-123",
  "path": "/third-party/mega",
  "cl": "12345",
  "mountpoint": "/var/lib/antares/mounts/550e8400-e29b-41d4-a716-446655440000",
  "layers": {
    "upper": "/var/lib/antares/upper/550e8400-e29b-41d4-a716-446655440000",
    "cl": "/var/lib/antares/cl/550e8400-e29b-41d4-a716-446655440000",
    "dicfuse": "shared"
  },
  "state": "Mounted",
  "created_at_epoch_ms": 1702800000000,
  "last_seen_epoch_ms": 1702800123456
}
```

**错误响应** (404 Not Found):
```json
{
  "error": "mount 550e8400-e29b-41d4-a716-446655440000 not found",
  "code": "NOT_FOUND"
}
```

---

### 4.1. 按任务 ID 查询挂载

**端点**: `GET /mounts/by-job/{job_id}`

**描述**: 通过 `job_id/build_id` 查询挂载详情(适用于“任务粒度”的挂载生命周期管理)。

**响应**: 同 `GET /mounts/{mount_id}`。

**错误响应** (404 Not Found):
```json
{
  "error": "mount for task job-123 not found",
  "code": "NOT_FOUND"
}
```

---

### 5. 删除挂载

**端点**: `DELETE /mounts/{mount_id}`

**描述**: 卸载并删除指定的 FUSE 挂载。

**路径参数**:
- `mount_id`: 挂载的 UUID

**响应** (200 OK):
```json
{
  "mount_id": "550e8400-e29b-41d4-a716-446655440000",
  "path": "/third-party/mega",
  "cl": "12345",
  "mountpoint": "/var/lib/antares/mounts/550e8400-e29b-41d4-a716-446655440000",
  "layers": {
    "upper": "/var/lib/antares/upper/550e8400-e29b-41d4-a716-446655440000",
    "cl": "/var/lib/antares/cl/550e8400-e29b-41d4-a716-446655440000",
    "dicfuse": "shared"
  },
  "state": "Unmounted",
  "created_at_epoch_ms": 1702800000000,
  "last_seen_epoch_ms": 1702800234567
}
```

**错误响应** (404 Not Found):
```json
{
  "error": "mount 550e8400-e29b-41d4-a716-446655440000 not found",
  "code": "NOT_FOUND"
}
```

**错误响应** (500 Internal Server Error - 卸载失败):
```json
{
  "mount_id": "550e8400-e29b-41d4-a716-446655440000",
  "state": {
    "Failed": {
      "reason": "unmount failed: Device or resource busy"
    }
  }
}
```

---

### 5.1. 按任务 ID 删除挂载

**端点**: `DELETE /mounts/by-job/{job_id}`

**描述**: 通过 `job_id/build_id` 卸载并删除挂载(更贴合构建系统的任务生命周期)。

**响应**: 返回被删除挂载的 `MountStatus`(`state` 为 `Unmounted`)。

**错误响应** (404 Not Found):
```json
{
  "error": "mount for task job-123 not found",
  "code": "NOT_FOUND"
}
```

---

## OpenAPI 3.0(摘要)

> 用于前端生成 client / 校验 schema。需要更完整 spec 时可再补齐 components/response schema。

```yaml
openapi: 3.0.3
info:
  title: Antares Mount API
  version: 1.0.0
servers:
  - url: http://127.0.0.1:2726
paths:
  /health:
    get:
      summary: Health check
      responses:
        "200":
          description: OK
  /mounts:
    get:
      summary: List mounts
      responses:
        "200":
          description: OK
    post:
      summary: Create mount (idempotent when job_id/build_id provided)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path]
              properties:
                job_id: { type: string }
                build_id: { type: string }
                path: { type: string }
                cl: { type: string }
      responses:
        "200":
          description: Created
  /mounts/{mount_id}:
    get:
      summary: Describe mount
      parameters:
        - in: path
          name: mount_id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: OK }
    delete:
      summary: Delete mount
      parameters:
        - in: path
          name: mount_id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: OK }
  /mounts/by-job/{job_id}:
    get:
      summary: Describe mount by job_id
      parameters:
        - in: path
          name: job_id
          required: true
          schema: { type: string }
      responses:
        "200": { description: OK }
    delete:
      summary: Delete mount by job_id
      parameters:
        - in: path
          name: job_id
          required: true
          schema: { type: string }
      responses:
        "200": { description: OK }
  /mounts/{mount_id}/cl:
    post:
      summary: Build/Rebuild CL layer
      parameters:
        - in: path
          name: mount_id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cl]
              properties:
                cl: { type: string }
      responses:
        "200": { description: OK }
    delete:
      summary: Clear CL layer
      parameters:
        - in: path
          name: mount_id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: OK }
```

---

## 典型用法(建议)

### 启动守护进程

```bash
# 在 scorpio 目录下
cargo run -p scorpio --bin antares -- serve --bind 0.0.0.0:2726
```

### 任务粒度挂载(推荐)

```bash
curl -sS -X POST http://127.0.0.1:2726/mounts \
  -H 'content-type: application/json' \
  -d '{"job_id":"job-123","path":"/third-party/mega","cl":"CL123"}'
```

重复调用同一个 `job_id` 将返回同一个挂载(幂等)。

### 任务粒度卸载(推荐)

```bash
curl -sS -X DELETE http://127.0.0.1:2726/mounts/by-job/job-123
```

### 使用 CLI 走 HTTP(推荐用于构建客户端)

```bash
cargo run -p scorpio --bin antares -- http-mount --endpoint http://127.0.0.1:2726 --job-id job-123 /third-party/mega --cl CL123
```

---

## 数据模型

### MountStatus

挂载状态的完整信息:

```rust
{
  "mount_id": "UUID",           // 挂载的唯一标识符
  "path": "String",             // monorepo 路径
  "cl": "Option<String>",       // 可选的 CL 标识符
  "mountpoint": "String",       // 实际的文件系统挂载点
  "layers": {
    "upper": "String",          // 上层目录路径
    "cl": "Option<String>",     // CL 层目录路径(可选)
    "dicfuse": "String"         // Dicfuse 层标识(通常为 "shared")
  },
  "state": "MountLifecycle",    // 挂载生命周期状态
  "created_at_epoch_ms": u64,   // 创建时间戳(毫秒)
  "last_seen_epoch_ms": u64     // 最后更新时间戳(毫秒)
}
```

### TypeScript 类型(可选)

前端/构建客户端使用 TypeScript,可以直接使用下面的类型定义(与 HTTP 返回 JSON 字段对齐):

```typescript
export interface CreateMountRequest {
  job_id?: string;
  build_id?: string;
  path: string;
  cl?: string;
}

export interface BuildClRequest {
  cl: string;
}

export interface MountCreated {
  mount_id: string; // UUID
  mountpoint: string;
}

export interface MountLayers {
  upper: string;
  cl: string | null;
  dicfuse: string;
}

export type MountLifecycle =
  | "Provisioning"
  | "Mounted"
  | "Unmounting"
  | "Unmounted"
  | { Failed: { reason: string } };

export interface MountStatus {
  mount_id: string; // UUID
  job_id: string | null;
  path: string;
  cl: string | null;
  mountpoint: string;
  layers: MountLayers;
  state: MountLifecycle;
  created_at_epoch_ms: number;
  last_seen_epoch_ms: number;
}

export interface MountCollection {
  mounts: MountStatus[];
}

export interface HealthResponse {
  status: "healthy" | "degraded";
  mount_count: number;
  uptime_secs: number;
}

export type ErrorCode =
  | "INVALID_REQUEST"
  | "BAD_PAYLOAD"
  | "NOT_FOUND"
  | "FUSE_ERROR"
  | "INTERNAL_ERROR"
  | "SHUTDOWN";

export interface ErrorBody {
  error: string;
  code: ErrorCode;
}
```

### MountLifecycle

挂载生命周期状态枚举:

- `"Provisioning"`: 正在准备中
- `"Mounted"`: 已挂载
- `"Unmounting"`: 正在卸载
- `"Unmounted"`: 已卸载
- `{"Failed": {"reason": "String"}}`: 失败(包含失败原因)

---

## 命令行工具

Antares 还提供命令行工具用于本地管理:

### 启动 HTTP 服务

```bash
antares --config scorpio.toml serve --bind 0.0.0.0:2726
```

### 挂载

```bash
antares mount <job_id> [--cl <cl_name>]
```

### 卸载

```bash
antares umount <job_id>
```

### 列表

```bash
antares list
```

---

## 配置

Antares 从 `scorpio.toml` 配置文件中读取以下配置项:

```toml
[antares]
mount_root = "/var/lib/antares/mounts"
upper_root = "/var/lib/antares/upper"
cl_root = "/var/lib/antares/cl"
state_file = "/var/lib/antares/state.toml"
```

可以通过命令行参数覆盖配置:
- `--mount-root`: 挂载点根目录
- `--upper-root`: 上层根目录
- `--cl-root`: CL 根目录
- `--state-file`: 状态持久化文件

---

## 错误代码

| 错误码 | HTTP 状态码 | 说明 |
|--------|------------|------|
| `INVALID_REQUEST` | 400 | 请求参数无效 |
| `NOT_FOUND` | 404 | 挂载不存在 |
| `FUSE_ERROR` | 500 | FUSE 操作失败 |
| `INTERNAL_ERROR` | 500 | 内部错误 |
| `BAD_PAYLOAD` | 400 | 请求体格式错误 |
| `SHUTDOWN` | 503 | 服务正在关闭 |

---

## 关键特性

### 并发性
- 每个挂载在独立的 Tokio 任务中运行
- Dicfuse 保持共享以避免重复下载数据

### 隔离性
- Upper/CL 目录对每个挂载唯一,写操作不会影响 Dicfuse

### 可观测性
调试时启用日志:
```bash
export RUST_LOG=libfuse_fs::passthrough::newlogfs=debug,rfuse3=trace
```

### 优雅关闭
服务器接收到 SIGINT (Ctrl+C) 信号时,会自动卸载所有挂载并清理资源。

### 清理
- 始终在删除挂载目录前先卸载文件系统,避免内核 I/O 错误
- 卸载失败的挂载会保留在状态中,标记为 `Failed` 状态

---

## 使用示例

### 创建挂载

```bash
curl -X POST http://localhost:2726/mounts \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/third-party/mega",
    "cl": "12345"
  }'
```

### 列出挂载

```bash
curl http://localhost:2726/mounts
```

### 查询挂载

```bash
curl http://localhost:2726/mounts/550e8400-e29b-41d4-a716-446655440000
```

### 删除挂载

```bash
curl -X DELETE http://localhost:2726/mounts/550e8400-e29b-41d4-a716-446655440000
```

### 健康检查

```bash
curl http://localhost:2726/health
```

### 客户端超时与重试建议

- `POST /mounts`:建议客户端 timeout 设为 **30–120s**(冷启动需要 Dicfuse 目录树 ready)
- `GET /mounts*`:建议 **5s**(纯查询)
- `DELETE /mounts*`:建议 **30s**(卸载可能等待)
- `POST /mounts/{id}/cl`:建议 **60s**(依赖外部 CL 服务)