Skip to content
Open
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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files need gofmt:"
echo "$unformatted"
exit 1
fi

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test -race -timeout 10m ./...
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
libraryTest.go
output.txt
bin/
data/
data/
meta/
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.PHONY: build test test-short test-race vet fmt clean

build:
go build -o bin/dfs-master ./cmd/master
go build -o bin/dfs-chunkserver ./cmd/chunkserver
go build -o bin/dfs-client ./cmd/client

test:
go test ./...

# Unit tests only; skips anything that binds a port.
test-short:
go test -short ./...

test-race:
go test -race -timeout 10m ./...

vet:
go vet ./...

fmt:
gofmt -w .

clean:
rm -rf bin data meta
85 changes: 77 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ go build -o bin/dfs-client ./cmd/client

| Feature | Description |
|---------|-------------|
| File Upload | Chunked upload with automatic allocation |
| File Download | Reassembly from distributed chunks |
| File Upload | Chunked upload written synchronously to every replica |
| File Download | Reassembly from distributed chunks, with replica failover |
| Durable Namespace | Write-ahead log + checkpoints; survives master restarts |
| Chunk Replication | Configurable replication factor (default: 2) |
| Heartbeat System | Bidirectional streaming for health monitoring |
| Dead Server Detection | Automatic detection and replication triggering |
| Replica Placement | Storage-aware server selection |
| Orphan Reclamation | Chunks with no namespace entry are collected after a grace period |
| Structured Logging | slog-based logging with levels |
| Client SDK | High-level API for file operations |
| CLI Tool | Command-line interface for put/get operations |
Expand All @@ -47,7 +49,21 @@ go build -o bin/dfs-client ./cmd/client
- Write pipeline (chain replication)
- Lease management for primary writes
- Checksum verification
- Graceful shutdown handling
- Delete / list / rename, and overwriting an existing file
- TLS and authentication between all components

### ⚠️ Known Limitations

- **No checksums.** The `checksum` field exists in the protocol but is always
zero, so bitrot and corrupt transfers go undetected.
- **Single master.** The namespace is durable, but the master is still a single
point of *availability* — there is no standby. Recovery means restarting it
over the same metadata directory.
- **No delete or overwrite.** Re-uploading an existing file name fails with
`AlreadyExists`.
- **No transport security.** All gRPC connections are insecure; anyone who can
reach the cluster can read or register.
- **No RPC deadlines on the client**, so an unresponsive server can hang the CLI.

---

Expand Down Expand Up @@ -127,16 +143,22 @@ sequenceDiagram
participant CS2 as ChunkServer 2

C->>M: AllocateChunk(filename, index)
M->>M: Append to namespace log (fsync)
M-->>C: ChunkID + ReplicaServers
C->>CS1: UploadChunk(stream)
CS1-->>C: Success

Note over M,CS2: Replication via heartbeat
C->>CS2: UploadChunk(stream)
CS2-->>C: Success

Note over M,CS2: Heartbeat repairs any replica lost later
M->>CS1: ReplicationTask
CS1->>CS2: ReplicateChunk
CS2-->>CS1: Ack
```

The client writes every replica before reporting success. Background replication
exists to repair replicas lost *after* the write, not to finish the write itself.

### Read Sequence

```mermaid
Expand Down Expand Up @@ -165,7 +187,9 @@ dfs/
├── internal/ # Private implementation
│ ├── master/ # Master server logic
│ │ └── metastore/ # Namespace write-ahead log + checkpoints
│ ├── chunkserver/ # Chunk server logic
│ ├── integration/ # End-to-end tests over real gRPC
│ └── client/ # Client SDK
│ ├── dfsclient.go # High-level SDK
│ ├── uploader/ # Upload handling
Expand Down Expand Up @@ -208,19 +232,58 @@ Flags:
-dir string Storage directory (default "./data")
```

### Master Flags

```bash
./bin/dfs-master [flags]

Flags:
-port string Address to listen on (default ":8000")
-meta string Namespace log and checkpoint directory (default "./meta")
-replication-factor int Target replicas per chunk (default 2)
-live-threshold duration Heartbeat silence before a server is presumed dead (default 30s)
-orphan-grace duration Delay after startup before reclaiming unknown chunks (default 5m)
```

---

## Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `REPLICATION_FACTOR` | 2 | Number of replicas per chunk |
| `-replication-factor` | 2 | Number of replicas per chunk |
| `CHUNK_SIZE` | 64 MB | Size of each chunk |
| `LIVE_THRESHOLD` | 30s | Server considered dead after this |
| `-live-threshold` | 30s | Server considered dead after this |
| `-orphan-grace` | 5m | Startup delay before collecting chunks with no metadata |
| Heartbeat Interval | 5s | Chunk server heartbeat frequency |

---

## Metadata Durability

The master follows the GFS operation-log design, which HDFS mirrors as
`edits` + `fsimage`. Metadata is split into two classes:

| | Stored where | Recovered how |
|---|---|---|
| **Namespace** (file → ordered chunk list) | Write-ahead log + checkpoints under `-meta` | Replayed at startup |
| **Chunk locations** (chunk → replica servers) | Nowhere — deliberately | Rebuilt from chunkserver registration and heartbeats |

Every namespace mutation is appended to `namespace.log` and `fsync`'d **before**
the RPC is acknowledged, so an acknowledged write survives a crash. Checkpoints
(`namespace.checkpoint`) are published by atomic rename and exist only to bound
replay time; the log remains the source of truth. A half-written trailing log
record is discarded on recovery — it was never `fsync`'d, so it was never
acknowledged to a client.

Replica locations are intentionally *not* persisted. Chunkservers are the only
authority on what they actually hold, so storing locations would just produce a
stale copy that disagrees with the disks. This is why a freshly restarted master
waits `-orphan-grace` before deleting chunks it has no metadata for: it must give
every chunkserver time to re-register first.

---

## Data Flow

### Write Path
Expand Down Expand Up @@ -260,9 +323,15 @@ Chunk servers maintain a bidirectional gRPC stream with the master:
# Build all
go build ./...

# Run tests
# Run tests (includes end-to-end tests that start real servers)
go test ./...

# Unit tests only — skips anything that binds ports
go test -short ./...

# Race detector
go test -race ./...

# Lint
go vet ./...
```
Expand Down
5 changes: 4 additions & 1 deletion cmd/chunkserver/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"flag"
"os"

"dfs/internal/chunkserver"
"dfs/pkg/logger"
"flag"
)

func main() {
Expand All @@ -24,5 +26,6 @@ func main() {

if err := cs.Start(); err != nil {
logger.Error("Chunk server failed", "error", err)
os.Exit(1)
}
}
26 changes: 25 additions & 1 deletion cmd/master/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
package main

import (
"flag"
"os"

"dfs/internal/master"
"dfs/pkg/logger"
)

func main() {
m := master.NewMasterServer()
cfg := master.DefaultConfig()

flag.StringVar(&cfg.ListenAddress, "port", cfg.ListenAddress, "address to listen on")
flag.StringVar(&cfg.MetadataDir, "meta", cfg.MetadataDir, "directory for the namespace log and checkpoints")
flag.IntVar(&cfg.ReplicationFactor, "replication-factor", cfg.ReplicationFactor, "target replicas per chunk")
flag.DurationVar(&cfg.LiveThreshold, "live-threshold", cfg.LiveThreshold, "time without a heartbeat before a chunkserver is presumed dead")
flag.DurationVar(&cfg.OrphanGracePeriod, "orphan-grace", cfg.OrphanGracePeriod, "time after startup before reclaiming chunks with no metadata")
flag.Parse()

if cfg.ReplicationFactor < 1 {
logger.Error("Invalid replication factor", "value", cfg.ReplicationFactor)
os.Exit(2)
}

m, err := master.NewMasterServer(cfg)
if err != nil {
logger.Error("Master server failed to start", "error", err)
os.Exit(1)
}
defer m.Close()

if err := m.Start(); err != nil {
logger.Error("Master server failed", "error", err)
os.Exit(1)
}
}
Loading
Loading