522 lines
17 KiB
Markdown
522 lines
17 KiB
Markdown
# meta-core
|
|
|
|
A Go-based sidecar binary that provides unified leader election, Redis management, and HTTP API for MetaMesh services.
|
|
|
|
## Overview
|
|
|
|
meta-core runs alongside each MetaMesh service (meta-sort, meta-fuse, meta-stremio) as a sidecar process managed by supervisord. It provides the foundational infrastructure that enables MetaMesh's distributed architecture without requiring external coordination services like etcd or Consul.
|
|
|
|
### Key Capabilities
|
|
|
|
| Feature | Description |
|
|
|---------|-------------|
|
|
| **Leader Election** | flock-based leader election (Go binary only runs as leader) |
|
|
| **Redis Management** | Leader spawns Redis with AOF+RDB persistence, auto-restart on crash |
|
|
| **HTTP API** | Language-agnostic REST interface for metadata and service discovery |
|
|
| **Service Discovery** | File-based registry with heartbeats and stale detection |
|
|
| **Metadata Storage** | Flat key-value schema with connection pooling and batch operations |
|
|
| **File Watching** | Directory scanning, MidHash256 computation, event dispatch |
|
|
| **Mount Management** | SMB/rclone configuration, health monitoring |
|
|
| **WebDAV Server** | File access with nginx proxy_cache |
|
|
| **Event Publishing** | Redis keyspace notifications to meta:events stream |
|
|
|
|
### Design Characteristics
|
|
|
|
- **No external dependencies** - Uses filesystem locks (works on NFS/CIFS shared volumes)
|
|
- **Automatic failover** - Lock releases on process death, followers detect and re-elect
|
|
- **Thread-safe** - RWMutex protection on all shared state
|
|
- **Graceful shutdown** - Signal handling with reverse startup order
|
|
- **Static binary** - CGO disabled, single binary deployment
|
|
|
|
## Architecture
|
|
|
|
meta-core runs as a **standalone container** that other services connect to via HTTP API:
|
|
|
|
```
|
|
┌───────────────────────────────────────────────────────────────────────┐
|
|
│ meta-core container (standalone) │
|
|
│ │
|
|
│ ┌──────────────┐ ┌─────────┐ ┌──────────┐ ┌────────────────────┐ │
|
|
│ │ Role │ │ Redis │ │ WebDAV │ │ HTTP API (:9000) │ │
|
|
│ │ Provider │ │ Server │ │ Server │ │ /meta, /urls, │ │
|
|
│ │ │ │ │ │ │ │ /health, /services │ │
|
|
│ └──────────────┘ └─────────┘ └──────────┘ └────────────────────┘ │
|
|
│ │
|
|
│ Writes: /meta-core/locks/kv-leader.info (URLs for all services) │
|
|
└──────────────────────────────────────────────┬────────────────────────┘
|
|
│
|
|
┌──────────────────────────────────────┴──────────────────────────┐
|
|
│ /meta-core (shared volume) │
|
|
│ ├── locks/kv-leader.info (leader URLs) │
|
|
│ ├── db/redis/ (Redis data) │
|
|
│ ├── services/ (hostname-based .json) │
|
|
│ └── mounts/ (mount configurations) │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
▲ ▲ ▲
|
|
┌────────────┘ │ └────────────┐
|
|
│ │ │
|
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
│ meta-sort │ │ meta-fuse │ │ meta-stremio │
|
|
│ (reads info, │ │ (reads info, │ │ (reads info, │
|
|
│ calls HTTP) │ │ calls HTTP) │ │ calls HTTP) │
|
|
└───────────────┘ └───────────────┘ └───────────────┘
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### Build
|
|
|
|
```bash
|
|
# Build binary
|
|
make build
|
|
|
|
# Build Docker image
|
|
make docker
|
|
|
|
# Run tests
|
|
make test
|
|
```
|
|
|
|
### Run
|
|
|
|
```bash
|
|
# With environment variables
|
|
META_CORE_PATH=/meta-core \
|
|
FILES_PATH=/files \
|
|
SERVICE_NAME=meta-sort \
|
|
META_CORE_HTTP_PORT=9000 \
|
|
./bin/meta-core
|
|
```
|
|
|
|
### Docker
|
|
|
|
```bash
|
|
docker build -t meta-core .
|
|
docker run -v meta-core:/meta-core -v files:/files meta-core
|
|
```
|
|
|
|
## Configuration
|
|
|
|
| Environment Variable | Default | Description |
|
|
|---------------------|---------|-------------|
|
|
| `META_CORE_PATH` | `/meta-core` | Shared volume for locks/db |
|
|
| `FILES_PATH` | `/files` | Shared volume for media files |
|
|
| `SERVICE_NAME` | `meta-core` | Service identifier |
|
|
| `SERVICE_VERSION` | `1.0.0` | Service version |
|
|
| `API_PORT` | `8180` | Main service HTTP port |
|
|
| `BASE_URL` | - | Stable service URL |
|
|
| `REDIS_PORT` | `6379` | Redis port (leader only) |
|
|
| `META_CORE_HTTP_PORT` | `9000` | HTTP API port |
|
|
| `META_CORE_HTTP_HOST` | `127.0.0.1` | HTTP API bind address |
|
|
| `HEALTH_CHECK_INTERVAL_MS` | `5000` | Health check interval |
|
|
| `HEARTBEAT_INTERVAL_MS` | `30000` | Service heartbeat interval |
|
|
| `STALE_THRESHOLD_MS` | `60000` | Stale service threshold |
|
|
| `ENABLE_FILE_WATCHER` | `true` | Enable file scanning |
|
|
|
|
## API Reference
|
|
|
|
### Health & Status
|
|
|
|
```bash
|
|
# Health check
|
|
curl http://localhost:9000/health
|
|
# {"status":"ok","redis":true,"timestamp":"..."}
|
|
|
|
# Detailed status
|
|
curl http://localhost:9000/status
|
|
# {"status":"ok","serviceName":"meta-core","version":"1.0.0",...}
|
|
|
|
# Current leader
|
|
curl http://localhost:9000/leader
|
|
# {"host":"meta-sort-dev","api":"redis://10.0.1.50:6379","http":"http://10.0.1.50:8180",...}
|
|
```
|
|
|
|
### Metadata Operations
|
|
|
|
```bash
|
|
# List all file hashes
|
|
curl http://localhost:9000/meta
|
|
# {"hashIds":["midhash256:abc123",...],"count":42}
|
|
|
|
# Get metadata for a file
|
|
curl http://localhost:9000/meta/{hash}
|
|
# {"hashId":"midhash256:abc123","metadata":{"title":"Movie","year":"2024",...}}
|
|
|
|
# Update metadata
|
|
curl -X PUT http://localhost:9000/meta/{hash} \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"title":"New Title"}'
|
|
|
|
# Delete metadata
|
|
curl -X DELETE http://localhost:9000/meta/{hash}
|
|
```
|
|
|
|
### Data Operations
|
|
|
|
```bash
|
|
# Get file path
|
|
curl http://localhost:9000/data/{hash}/path
|
|
# {"hashId":"midhash256:abc123","path":"/files/movies/Movie.mkv","exists":true}
|
|
|
|
# Check if file exists
|
|
curl -I http://localhost:9000/data/{hash}
|
|
# HTTP/1.1 200 OK (or 404)
|
|
```
|
|
|
|
### File Operations (by CID)
|
|
|
|
```bash
|
|
# Serve file by CID (looks up poster/backdrop CIDs in metadata)
|
|
curl http://localhost:9000/file/{cid} --output poster.jpg
|
|
# Returns raw file bytes with appropriate Content-Type header
|
|
|
|
# Example: fetch a poster image
|
|
curl -v http://localhost:9000/file/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garber7ndsuxku4
|
|
# Content-Type: image/jpeg
|
|
# [binary image data]
|
|
```
|
|
|
|
The `/file/{cid}` endpoint searches all file metadata for matching `poster` or `backdrop` CIDs and serves the corresponding file from disk. Supports range requests for partial content retrieval.
|
|
|
|
### Service Discovery
|
|
|
|
```bash
|
|
# List all services
|
|
curl http://localhost:9000/services
|
|
# {"services":[{"name":"meta-sort",...}],"count":3}
|
|
|
|
# Get specific service
|
|
curl http://localhost:9000/services/meta-sort
|
|
# {"name":"meta-sort","version":"2.0.0","api":"http://...","status":"running",...}
|
|
```
|
|
|
|
### Mount Management
|
|
|
|
```bash
|
|
# List all mounts
|
|
curl http://localhost:9000/api/mounts/list
|
|
# {"mounts":[{"id":"smb-nas","type":"smb","path":"/files/corn/nas",...}]}
|
|
|
|
# Add a new mount
|
|
curl -X POST http://localhost:9000/api/mounts/add \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"type":"smb","path":"/files/corn/nas","config":{...}}'
|
|
|
|
# Remove a mount
|
|
curl -X DELETE http://localhost:9000/api/mounts/smb-nas
|
|
```
|
|
|
|
### File Watching
|
|
|
|
```bash
|
|
# Get scan status
|
|
curl http://localhost:9000/api/scan/status
|
|
# {"scanning":false,"lastScan":"2024-01-15T10:30:00Z","filesFound":12847}
|
|
|
|
# Trigger rescan
|
|
curl -X POST http://localhost:9000/api/scan/trigger
|
|
|
|
# Stream file events (SSE)
|
|
curl http://localhost:9000/api/events/stream
|
|
# data: {"type":"add","path":"movies/Movie.mkv","midhash256":"bafkr..."}
|
|
```
|
|
|
|
### KV Browser
|
|
|
|
```bash
|
|
# Storage statistics
|
|
curl http://localhost:9000/api/kv/info
|
|
# {"totalKeys":50000,"memoryUsed":"256MB",...}
|
|
|
|
# List keys with cursor pagination
|
|
curl "http://localhost:9000/api/kv/keys?cursor=0&count=100"
|
|
|
|
# Get specific key value
|
|
curl http://localhost:9000/api/kv/key/file:bafkr.../title
|
|
```
|
|
|
|
### Metadata Editor
|
|
|
|
```bash
|
|
# Get all hash IDs
|
|
curl http://localhost:9000/api/metadata/hash-ids
|
|
# {"hashIds":["bafkr...","bafks..."]}
|
|
|
|
# Paginated metadata listing
|
|
curl "http://localhost:9000/api/metadata/list?page=0&pageSize=50"
|
|
|
|
# Search metadata
|
|
curl -X POST http://localhost:9000/api/metadata/search \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query":"inception","fields":["title"]}'
|
|
|
|
# Batch update
|
|
curl -X POST http://localhost:9000/api/metadata/batch \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"operations":[{"hashId":"bafkr...","updates":{"title":"New Title"}}]}'
|
|
|
|
# Clear all metadata
|
|
curl -X POST http://localhost:9000/api/metadata/clear
|
|
```
|
|
|
|
### WebDAV Access
|
|
|
|
```bash
|
|
# List directory (returns JSON)
|
|
curl http://localhost:9000/webdav/
|
|
# [{"name":"watch","isDir":true},{"name":"plugin","isDir":true}]
|
|
|
|
# Get file
|
|
curl http://localhost:9000/webdav/watch/movie.mkv --output movie.mkv
|
|
|
|
# Upload file
|
|
curl -X PUT http://localhost:9000/webdav/plugin/output.txt \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary @output.txt
|
|
|
|
# Create directory
|
|
curl -X MKCOL http://localhost:9000/webdav/newdir/
|
|
|
|
# Delete file/directory
|
|
curl -X DELETE http://localhost:9000/webdav/oldfile.txt
|
|
```
|
|
|
|
## Leader Election
|
|
|
|
meta-core uses POSIX file locking (flock) for distributed leader election:
|
|
|
|
1. Each instance tries to acquire exclusive lock on `/meta-core/locks/kv-leader.lock`
|
|
2. Winner becomes **leader**, spawns Redis, writes info to `/meta-core/locks/kv-leader.info`
|
|
3. Losers become **followers**, read leader info, connect to leader's Redis
|
|
4. Lock automatically releases when process dies (no stale locks)
|
|
|
|
### Election Flow
|
|
|
|
```
|
|
Instance 1 Instance 2 Instance 3
|
|
│ │ │
|
|
├── Try flock ──────────┼───────────────────────┤
|
|
│ │ │
|
|
[WIN] [LOSE] [LOSE]
|
|
LEADER FOLLOWER FOLLOWER
|
|
│ │ │
|
|
Start Redis Read leader.info Read leader.info
|
|
│ │ │
|
|
Write leader.info Connect to Redis Connect to Redis
|
|
│ │ │
|
|
[Health loop 5s] [Health loop 5s] [Health loop 5s]
|
|
- Update timestamp - Re-read leader - Re-read leader
|
|
- Check Redis alive - Detect changes - Detect changes
|
|
```
|
|
|
|
### Role Callbacks
|
|
|
|
The election system provides callbacks for role transitions:
|
|
|
|
```go
|
|
election.OnBecomeLeader(func(info LeaderLockInfo) {
|
|
// Start Redis, initialize resources
|
|
})
|
|
election.OnBecomeFollower(func(info LeaderLockInfo) {
|
|
// Connect to leader's Redis
|
|
})
|
|
election.OnLeaderLost(func() {
|
|
// Handle leader failure, attempt re-election
|
|
})
|
|
```
|
|
|
|
### Lock Info Format
|
|
|
|
The `kv-leader.info` file contains a plain text API URL:
|
|
|
|
```
|
|
http://10.0.1.50:9000
|
|
```
|
|
|
|
Services read this file to discover the leader, then call the `/urls` API endpoint to get full connection details (Redis URL, WebDAV URL, base URL, etc.).
|
|
|
|
## Redis Management
|
|
|
|
The leader is responsible for spawning and managing the Redis server.
|
|
|
|
### Redis Configuration
|
|
|
|
```bash
|
|
redis-server --port 6379 --bind 0.0.0.0 \
|
|
--dir /meta-core/db/redis \
|
|
--appendonly yes --appendfilename appendonly.aof \
|
|
--dbfilename dump.rdb --save 60 1 \
|
|
--loglevel warning
|
|
```
|
|
|
|
### Persistence Strategy
|
|
|
|
| Method | Configuration | Purpose |
|
|
|--------|--------------|---------|
|
|
| **AOF** | `appendonly yes` | Write-ahead log for durability |
|
|
| **RDB** | `save 60 1` | Snapshot after 60s if ≥1 key changed |
|
|
|
|
### Failure Handling
|
|
|
|
- **Redis crash**: Auto-detected via health check, automatically restarted
|
|
- **Leader crash**: Lock released, followers detect via stale timestamp, re-election occurs
|
|
- **Graceful shutdown**: SIGTERM sent to Redis (10s timeout), then SIGKILL if needed
|
|
|
|
## Metadata Storage
|
|
|
|
meta-core uses a flat key-value schema in Redis for storing file metadata.
|
|
|
|
### Storage Format
|
|
|
|
```
|
|
Key: /file/{hashId}/{property}
|
|
Value: string
|
|
|
|
Examples:
|
|
/file/midhash256:abc123/title → "Movie Title"
|
|
/file/midhash256:abc123/year → "2024"
|
|
/file/midhash256:abc123/genres → "action,drama"
|
|
/file/midhash256:abc123/filePath → "movies/Movie.mkv"
|
|
```
|
|
|
|
### Storage Client Features
|
|
|
|
| Feature | Details |
|
|
|---------|---------|
|
|
| Connection Pool | 10 connections |
|
|
| Timeouts | 5s dial, 30s read/write |
|
|
| Batch Operations | Pipelined writes for performance |
|
|
| Key Scanning | SCAN-based iteration (non-blocking) |
|
|
| CID Lookup | Find file paths by poster/backdrop CID |
|
|
|
|
## Service Discovery
|
|
|
|
Services register by writing JSON to `/meta-core/services/{name}.json`.
|
|
|
|
### Registration Format
|
|
|
|
```json
|
|
{
|
|
"name": "meta-sort",
|
|
"version": "2.0.0",
|
|
"api": "http://10.0.1.50:8180",
|
|
"status": "running",
|
|
"pid": 12345,
|
|
"hostname": "meta-sort-dev",
|
|
"startedAt": "2024-01-01T00:00:00Z",
|
|
"lastHeartbeat": "2024-01-01T00:01:00Z",
|
|
"capabilities": ["meta-core"],
|
|
"endpoints": {
|
|
"health": "http://10.0.1.50:9000/health",
|
|
"meta": "http://10.0.1.50:9000/meta"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Heartbeat & Stale Detection
|
|
|
|
| Parameter | Default | Purpose |
|
|
|-----------|---------|---------|
|
|
| `HEARTBEAT_INTERVAL_MS` | 30000 | How often services update `lastHeartbeat` |
|
|
| `STALE_THRESHOLD_MS` | 60000 | Mark service "stale" if heartbeat older than this |
|
|
|
|
Services are automatically marked as `stale` when discovered if their `lastHeartbeat` exceeds the threshold. On graceful shutdown, services remove their registration file.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Format code
|
|
make fmt
|
|
|
|
# Run linter
|
|
make lint
|
|
|
|
# Run tests with coverage
|
|
make test-cover
|
|
|
|
# Clean build artifacts
|
|
make clean
|
|
```
|
|
|
|
## Internal Packages
|
|
|
|
| Package | Purpose |
|
|
|---------|---------|
|
|
| `cmd/meta-core` | Main entry point, startup/shutdown orchestration |
|
|
| `internal/config` | Environment-based configuration with defaults |
|
|
| `internal/leader` | Election system (`election.go`) and Redis manager (`redis.go`) |
|
|
| `internal/storage` | Redis client wrapper with flat key-value operations |
|
|
| `internal/discovery` | Service registration, heartbeat loop, discovery |
|
|
| `internal/api` | HTTP server, router, and all endpoint handlers |
|
|
| `internal/events` | Redis keyspace notification publisher to meta:events |
|
|
| `internal/watcher` | File system scanning, MidHash256 computation, event dispatch |
|
|
| `internal/watchers` | Polling-based watcher configuration management |
|
|
| `internal/mounts` | SMB/rclone mount configuration and health monitoring |
|
|
| `internal/webdav` | WebDAV protocol handler with caching integration |
|
|
|
|
### Startup Sequence
|
|
|
|
```
|
|
1. Load configuration from environment
|
|
2. Create storage client (Redis wrapper)
|
|
3. Initialize leader election
|
|
4. Register role transition callbacks
|
|
5. Start election (acquire lock or become follower)
|
|
6. Start service discovery (register + heartbeat loop)
|
|
7. Initialize dead service cleaner
|
|
8. Initialize file watcher (if enabled)
|
|
9. Initialize mount manager
|
|
10. Start HTTP API server (WebDAV caching handled by nginx)
|
|
11. Wait for SIGINT/SIGTERM
|
|
12. Shutdown in reverse order
|
|
```
|
|
|
|
## Integration
|
|
|
|
### As Sidecar in Docker
|
|
|
|
Add to existing service's supervisord config:
|
|
|
|
```ini
|
|
[program:meta-core]
|
|
command=/usr/local/bin/meta-core
|
|
priority=10
|
|
autostart=true
|
|
autorestart=true
|
|
```
|
|
|
|
### Client Usage (TypeScript)
|
|
|
|
```typescript
|
|
const response = await fetch('http://localhost:9000/meta/' + hashId);
|
|
const { metadata } = await response.json();
|
|
```
|
|
|
|
### Client Usage (Python)
|
|
|
|
```python
|
|
import requests
|
|
response = requests.get(f'http://localhost:9000/meta/{hash_id}')
|
|
metadata = response.json()['metadata']
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
| Dependency | Version | Purpose |
|
|
|------------|---------|---------|
|
|
| `github.com/gorilla/mux` | v1.8.1 | HTTP router |
|
|
| `github.com/redis/go-redis/v9` | v9.4.0 | Redis client |
|
|
| `golang.org/x/net` | v0.20.0 | WebDAV support |
|
|
| `github.com/google/uuid` | v1.6.0 | UUID generation |
|
|
| Go stdlib | - | context, encoding/json, net, os, sync, syscall |
|
|
|
|
### Runtime Requirements
|
|
|
|
- **Go 1.21+** for building
|
|
- **Redis** binary available in PATH (leader spawns it)
|
|
- **Shared filesystem** accessible by all instances (for flock)
|
|
|
|
## License
|
|
|
|
Part of the MetaMesh project.
|