Compare commits
3 Commits
metadata-a
...
self-daemo
Author | SHA1 | Date | |
---|---|---|---|
64680a6ece | |||
4c3fcbc45a | |||
a41e0d625c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
.claude/
|
.claude/
|
||||||
|
.kvs/
|
||||||
data/
|
data/
|
||||||
data*/
|
data*/
|
||||||
|
integration_test/
|
||||||
*.yaml
|
*.yaml
|
||||||
!config.yaml
|
!config.yaml
|
||||||
kvs
|
kvs
|
||||||
|
76
CLAUDE.md
76
CLAUDE.md
@@ -10,10 +10,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
go build -o kvs .
|
go build -o kvs .
|
||||||
|
|
||||||
# Run with default config (auto-generates config.yaml)
|
# Run with default config (auto-generates config.yaml)
|
||||||
./kvs
|
./kvs start config.yaml
|
||||||
|
|
||||||
# Run with custom config
|
# Run with custom config
|
||||||
./kvs /path/to/config.yaml
|
./kvs start /path/to/config.yaml
|
||||||
|
|
||||||
|
# Check running instances
|
||||||
|
./kvs status
|
||||||
|
|
||||||
|
# Stop instance
|
||||||
|
./kvs stop config
|
||||||
|
|
||||||
# Run comprehensive integration tests
|
# Run comprehensive integration tests
|
||||||
./integration_test.sh
|
./integration_test.sh
|
||||||
@@ -25,6 +31,32 @@ go run test_conflict.go data1 data2
|
|||||||
go build -o kvs . && ./integration_test.sh
|
go build -o kvs . && ./integration_test.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Process Management Commands
|
||||||
|
```bash
|
||||||
|
# Start as background daemon
|
||||||
|
./kvs start <config.yaml> # .yaml extension optional
|
||||||
|
|
||||||
|
# Stop daemon
|
||||||
|
./kvs stop <config> # Graceful SIGTERM shutdown
|
||||||
|
|
||||||
|
# Restart daemon
|
||||||
|
./kvs restart <config> # Stop then start
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
./kvs status # All instances
|
||||||
|
./kvs status <config> # Specific instance
|
||||||
|
|
||||||
|
# Run in foreground (for debugging)
|
||||||
|
./kvs <config.yaml> # Logs to stdout, blocks terminal
|
||||||
|
|
||||||
|
# View daemon logs
|
||||||
|
tail -f ~/.kvs/logs/kvs_<config>.yaml.log
|
||||||
|
|
||||||
|
# Global state directories
|
||||||
|
~/.kvs/pids/ # PID files (works from any directory)
|
||||||
|
~/.kvs/logs/ # Daemon log files
|
||||||
|
```
|
||||||
|
|
||||||
### Development Workflow
|
### Development Workflow
|
||||||
```bash
|
```bash
|
||||||
# Format and check code
|
# Format and check code
|
||||||
@@ -38,11 +70,25 @@ go mod tidy
|
|||||||
go build .
|
go build .
|
||||||
|
|
||||||
# Test specific cluster scenarios
|
# Test specific cluster scenarios
|
||||||
./kvs node1.yaml & # Terminal 1
|
./kvs start node1.yaml
|
||||||
./kvs node2.yaml & # Terminal 2
|
./kvs start node2.yaml
|
||||||
|
|
||||||
|
# Wait for cluster formation
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Test data operations
|
||||||
curl -X PUT http://localhost:8081/kv/test/data -H "Content-Type: application/json" -d '{"test":"data"}'
|
curl -X PUT http://localhost:8081/kv/test/data -H "Content-Type: application/json" -d '{"test":"data"}'
|
||||||
curl http://localhost:8082/kv/test/data # Should replicate within ~30 seconds
|
curl http://localhost:8082/kv/test/data # Should replicate within ~30 seconds
|
||||||
pkill kvs
|
|
||||||
|
# Check daemon status
|
||||||
|
./kvs status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f ~/.kvs/logs/kvs_node1.yaml.log
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
./kvs stop node1
|
||||||
|
./kvs stop node2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
@@ -58,7 +104,8 @@ KVS is a **distributed, eventually consistent key-value store** built around thr
|
|||||||
|
|
||||||
#### Modular Package Design
|
#### Modular Package Design
|
||||||
- **`auth/`** - Complete JWT authentication system with POSIX-inspired permissions
|
- **`auth/`** - Complete JWT authentication system with POSIX-inspired permissions
|
||||||
- **`cluster/`** - Distributed systems logic (gossip, sync, merkle trees)
|
- **`cluster/`** - Distributed systems logic (gossip, sync, merkle trees)
|
||||||
|
- **`daemon/`** - Process management (daemonization, PID files, lifecycle)
|
||||||
- **`storage/`** - BadgerDB abstraction with compression and revision history
|
- **`storage/`** - BadgerDB abstraction with compression and revision history
|
||||||
- **`server/`** - HTTP handlers, routing, and lifecycle management
|
- **`server/`** - HTTP handlers, routing, and lifecycle management
|
||||||
- **`features/`** - Utility functions for TTL, rate limiting, tamper logging, backup
|
- **`features/`** - Utility functions for TTL, rate limiting, tamper logging, backup
|
||||||
@@ -147,9 +194,18 @@ Creates two BadgerDB instances with intentionally conflicting data (same path, s
|
|||||||
- **Bootstrap sync**: Up to 30 days of historical data for new nodes
|
- **Bootstrap sync**: Up to 30 days of historical data for new nodes
|
||||||
|
|
||||||
#### Main Entry Point Flow
|
#### Main Entry Point Flow
|
||||||
1. `main.go` loads config (auto-generates default if missing)
|
1. `main.go` parses command-line arguments for subcommands (`start`, `stop`, `status`, `restart`)
|
||||||
2. `server.NewServer()` initializes all subsystems
|
2. For daemon mode: `daemon.Daemonize()` spawns background process and manages PID files
|
||||||
3. Graceful shutdown handling with `SIGINT`/`SIGTERM`
|
3. For server mode: loads config (auto-generates default if missing)
|
||||||
4. All business logic delegated to modular packages
|
4. `server.NewServer()` initializes all subsystems
|
||||||
|
5. Graceful shutdown handling with `SIGINT`/`SIGTERM`
|
||||||
|
6. All business logic delegated to modular packages
|
||||||
|
|
||||||
|
#### Daemon Architecture
|
||||||
|
- **PID Management**: Global PID files stored in `~/.kvs/pids/` for cross-directory access
|
||||||
|
- **Logging**: Daemon logs written to `~/.kvs/logs/{config-name}.log`
|
||||||
|
- **Process Lifecycle**: Spawns detached process via `exec.Command()` with `Setsid: true`
|
||||||
|
- **Config Normalization**: Supports both `node1` and `node1.yaml` formats
|
||||||
|
- **Stale PID Detection**: Checks process existence via `Signal(0)` before operations
|
||||||
|
|
||||||
This architecture enables easy feature addition, comprehensive testing, and reliable operation in distributed environments while maintaining simplicity for single-node deployments.
|
This architecture enables easy feature addition, comprehensive testing, and reliable operation in distributed environments while maintaining simplicity for single-node deployments.
|
115
README.md
115
README.md
@@ -69,11 +69,67 @@ go build -o kvs .
|
|||||||
|
|
||||||
### Quick Test
|
### Quick Test
|
||||||
```bash
|
```bash
|
||||||
# Start standalone node
|
# Start standalone node (uses config.yaml if it exists, or creates it)
|
||||||
./kvs
|
./kvs start config.yaml
|
||||||
|
|
||||||
# Test the API
|
# Test the API
|
||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
./kvs status
|
||||||
|
|
||||||
|
# Stop when done
|
||||||
|
./kvs stop config
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Process Management
|
||||||
|
|
||||||
|
KVS includes systemd-style daemon commands for easy process management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start as background daemon
|
||||||
|
./kvs start config.yaml # or just: ./kvs start config
|
||||||
|
./kvs start node1.yaml # Start with custom config
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
./kvs status # Show all running instances
|
||||||
|
./kvs status node1 # Show specific instance
|
||||||
|
|
||||||
|
# Stop daemon
|
||||||
|
./kvs stop node1 # Graceful shutdown
|
||||||
|
|
||||||
|
# Restart daemon
|
||||||
|
./kvs restart node1 # Stop and start
|
||||||
|
|
||||||
|
# Run in foreground (traditional)
|
||||||
|
./kvs node1.yaml # Logs to stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daemon Features
|
||||||
|
- **Global PID tracking**: PID files stored in `~/.kvs/pids/` (works from any directory)
|
||||||
|
- **Automatic logging**: Logs written to `~/.kvs/logs/{config-name}.log`
|
||||||
|
- **Flexible naming**: Config extension optional (`node1` or `node1.yaml` both work)
|
||||||
|
- **Graceful shutdown**: SIGTERM sent for clean shutdown
|
||||||
|
- **Stale PID cleanup**: Automatically detects and cleans dead processes
|
||||||
|
- **Multi-instance**: Run multiple KVS instances on same machine
|
||||||
|
|
||||||
|
### Example Workflow
|
||||||
|
```bash
|
||||||
|
# Start 3-node cluster as daemons
|
||||||
|
./kvs start node1.yaml
|
||||||
|
./kvs start node2.yaml
|
||||||
|
./kvs start node3.yaml
|
||||||
|
|
||||||
|
# Check cluster status
|
||||||
|
./kvs status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f ~/.kvs/logs/kvs_node1.yaml.log
|
||||||
|
|
||||||
|
# Stop entire cluster
|
||||||
|
./kvs stop node1
|
||||||
|
./kvs stop node2
|
||||||
|
./kvs stop node3
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
@@ -308,17 +364,23 @@ clustering_enabled: true
|
|||||||
|
|
||||||
#### Start the Cluster
|
#### Start the Cluster
|
||||||
```bash
|
```bash
|
||||||
# Terminal 1
|
# Start as daemons
|
||||||
./kvs node1.yaml
|
./kvs start node1.yaml
|
||||||
|
sleep 2
|
||||||
# Terminal 2 (wait a few seconds)
|
./kvs start node2.yaml
|
||||||
./kvs node2.yaml
|
sleep 2
|
||||||
|
./kvs start node3.yaml
|
||||||
# Terminal 3 (wait a few seconds)
|
|
||||||
./kvs node3.yaml
|
|
||||||
|
|
||||||
# Verify cluster formation
|
# Verify cluster formation
|
||||||
curl http://localhost:8081/members/ # Should show all 3 nodes
|
curl http://localhost:8081/members/ # Should show all 3 nodes
|
||||||
|
|
||||||
|
# Check daemon status
|
||||||
|
./kvs status
|
||||||
|
|
||||||
|
# Stop cluster when done
|
||||||
|
./kvs stop node1
|
||||||
|
./kvs stop node2
|
||||||
|
./kvs stop node3
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 How It Works
|
## 🔄 How It Works
|
||||||
@@ -364,9 +426,10 @@ go build -o kvs .
|
|||||||
./integration_test.sh
|
./integration_test.sh
|
||||||
|
|
||||||
# Manual basic functionality test
|
# Manual basic functionality test
|
||||||
./kvs &
|
./kvs start config.yaml
|
||||||
|
sleep 2
|
||||||
curl http://localhost:8080/health
|
curl http://localhost:8080/health
|
||||||
pkill kvs
|
./kvs stop config
|
||||||
|
|
||||||
# Manual cluster test (requires creating configs)
|
# Manual cluster test (requires creating configs)
|
||||||
echo 'node_id: "test1"
|
echo 'node_id: "test1"
|
||||||
@@ -379,8 +442,9 @@ port: 8082
|
|||||||
seed_nodes: ["127.0.0.1:8081"]
|
seed_nodes: ["127.0.0.1:8081"]
|
||||||
auth_enabled: false' > test2.yaml
|
auth_enabled: false' > test2.yaml
|
||||||
|
|
||||||
./kvs test1.yaml &
|
./kvs start test1.yaml
|
||||||
./kvs test2.yaml &
|
sleep 2
|
||||||
|
./kvs start test2.yaml
|
||||||
|
|
||||||
# Test data replication (wait for cluster formation)
|
# Test data replication (wait for cluster formation)
|
||||||
sleep 10
|
sleep 10
|
||||||
@@ -393,7 +457,8 @@ sleep 30
|
|||||||
curl http://localhost:8082/kv/test/data
|
curl http://localhost:8082/kv/test/data
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
pkill kvs
|
./kvs stop test1
|
||||||
|
./kvs stop test2
|
||||||
rm test1.yaml test2.yaml
|
rm test1.yaml test2.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -418,17 +483,22 @@ auth_enabled: false
|
|||||||
log_level: "debug"' > conflict2.yaml
|
log_level: "debug"' > conflict2.yaml
|
||||||
|
|
||||||
# Start nodes with conflicting data
|
# Start nodes with conflicting data
|
||||||
./kvs conflict1.yaml &
|
./kvs start conflict1.yaml
|
||||||
./kvs conflict2.yaml &
|
sleep 2
|
||||||
|
./kvs start conflict2.yaml
|
||||||
|
|
||||||
# Watch logs for conflict resolution
|
# Watch logs for conflict resolution
|
||||||
|
tail -f ~/.kvs/logs/kvs_conflict1.yaml.log ~/.kvs/logs/kvs_conflict2.yaml.log &
|
||||||
|
|
||||||
# Both nodes will converge within ~10-30 seconds
|
# Both nodes will converge within ~10-30 seconds
|
||||||
# Check final state
|
# Check final state
|
||||||
sleep 30
|
sleep 30
|
||||||
curl http://localhost:9111/kv/test/conflict/data
|
curl http://localhost:9111/kv/test/conflict/data
|
||||||
curl http://localhost:9112/kv/test/conflict/data
|
curl http://localhost:9112/kv/test/conflict/data
|
||||||
|
|
||||||
pkill kvs
|
# Cleanup
|
||||||
|
./kvs stop conflict1
|
||||||
|
./kvs stop conflict2
|
||||||
rm conflict1.yaml conflict2.yaml
|
rm conflict1.yaml conflict2.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -474,6 +544,10 @@ kvs/
|
|||||||
├── config/ # Configuration management
|
├── config/ # Configuration management
|
||||||
│ └── config.go # Config loading & defaults
|
│ └── config.go # Config loading & defaults
|
||||||
│
|
│
|
||||||
|
├── daemon/ # Process management
|
||||||
|
│ ├── daemonize.go # Background process spawning
|
||||||
|
│ └── pid.go # PID file management
|
||||||
|
│
|
||||||
├── features/ # Utility features
|
├── features/ # Utility features
|
||||||
│ ├── auth.go # Auth utilities
|
│ ├── auth.go # Auth utilities
|
||||||
│ ├── backup.go # Backup system
|
│ ├── backup.go # Backup system
|
||||||
@@ -580,8 +654,9 @@ type StoredValue struct {
|
|||||||
## 🛡️ Production Considerations
|
## 🛡️ Production Considerations
|
||||||
|
|
||||||
### Deployment
|
### Deployment
|
||||||
- Use systemd or similar for process management
|
- Built-in daemon commands (`start`/`stop`/`restart`/`status`) for process management
|
||||||
- Configure log rotation for JSON logs
|
- Alternatively, use systemd or similar for advanced orchestration
|
||||||
|
- Logs automatically written to `~/.kvs/logs/` (configure log rotation)
|
||||||
- Set up monitoring for `/health` endpoint
|
- Set up monitoring for `/health` endpoint
|
||||||
- Use reverse proxy (nginx/traefik) for TLS and load balancing
|
- Use reverse proxy (nginx/traefik) for TLS and load balancing
|
||||||
|
|
||||||
|
87
daemon/daemonize.go
Normal file
87
daemon/daemonize.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLogFilePath returns the log file path for a given config file
|
||||||
|
func GetLogFilePath(configPath string) (string, error) {
|
||||||
|
logDir, err := getLogDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
absConfigPath, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get absolute config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basename := filepath.Base(configPath)
|
||||||
|
name := filepath.Base(filepath.Dir(absConfigPath)) + "_" + basename
|
||||||
|
return filepath.Join(logDir, name+".log"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemonize spawns the process as a daemon and returns
|
||||||
|
func Daemonize(configPath string) error {
|
||||||
|
// Get absolute path to the current executable
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get absolute path to config
|
||||||
|
absConfigPath, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already running
|
||||||
|
_, running, err := ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if instance is running: %w", err)
|
||||||
|
}
|
||||||
|
if running {
|
||||||
|
return fmt.Errorf("instance is already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the process in background with --daemon flag
|
||||||
|
cmd := exec.Command(executable, "--daemon", absConfigPath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true, // Create new session
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stdout/stderr to log file
|
||||||
|
logDir, err := getLogDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get log directory: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create log directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basename := filepath.Base(configPath)
|
||||||
|
name := filepath.Base(filepath.Dir(absConfigPath)) + "_" + basename
|
||||||
|
logFile := filepath.Join(logDir, name+".log")
|
||||||
|
|
||||||
|
f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
cmd.Stdout = f
|
||||||
|
cmd.Stderr = f
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start daemon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Started KVS instance '%s' (PID will be written by daemon)\n", filepath.Base(configPath))
|
||||||
|
fmt.Printf("Logs: %s\n", logFile)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
171
daemon/pid.go
Normal file
171
daemon/pid.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getPIDDir returns the absolute path to the PID directory
|
||||||
|
func getPIDDir() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".kvs", "pids"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogDir returns the absolute path to the log directory
|
||||||
|
func getLogDir() (string, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".kvs", "logs"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPIDFilePath returns the PID file path for a given config file
|
||||||
|
func GetPIDFilePath(configPath string) string {
|
||||||
|
pidDir, err := getPIDDir()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to local directory
|
||||||
|
pidDir = ".kvs/pids"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract basename without extension
|
||||||
|
basename := filepath.Base(configPath)
|
||||||
|
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
|
||||||
|
return filepath.Join(pidDir, name+".pid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsurePIDDir creates the PID directory if it doesn't exist
|
||||||
|
func EnsurePIDDir() error {
|
||||||
|
pidDir, err := getPIDDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.MkdirAll(pidDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritePID writes the current process PID to a file
|
||||||
|
func WritePID(configPath string) error {
|
||||||
|
if err := EnsurePIDDir(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create PID directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pidFile := GetPIDFilePath(configPath)
|
||||||
|
pid := os.Getpid()
|
||||||
|
|
||||||
|
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", pid)), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPID reads the PID from a file and checks if the process is running
|
||||||
|
func ReadPID(configPath string) (int, bool, error) {
|
||||||
|
pidFile := GetPIDFilePath(configPath)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(pidFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
return 0, false, fmt.Errorf("failed to read PID file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pidStr := strings.TrimSpace(string(data))
|
||||||
|
pid, err := strconv.Atoi(pidStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, fmt.Errorf("invalid PID in file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if process is actually running
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return pid, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send signal 0 to check if process exists
|
||||||
|
err = process.Signal(syscall.Signal(0))
|
||||||
|
if err != nil {
|
||||||
|
return pid, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePID removes the PID file
|
||||||
|
func RemovePID(configPath string) error {
|
||||||
|
pidFile := GetPIDFilePath(configPath)
|
||||||
|
err := os.Remove(pidFile)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to remove PID file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRunningInstances returns a list of running KVS instances
|
||||||
|
func ListRunningInstances() ([]InstanceInfo, error) {
|
||||||
|
var instances []InstanceInfo
|
||||||
|
|
||||||
|
pidDir, err := getPIDDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if PID directory exists
|
||||||
|
if _, err := os.Stat(pidDir); os.IsNotExist(err) {
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(pidDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read PID directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(entry.Name(), ".pid")
|
||||||
|
configPath := name + ".yaml" // Assume .yaml extension
|
||||||
|
|
||||||
|
pid, running, err := ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
instances = append(instances, InstanceInfo{
|
||||||
|
Name: name,
|
||||||
|
PID: pid,
|
||||||
|
Running: running,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstanceInfo holds information about a KVS instance
|
||||||
|
type InstanceInfo struct {
|
||||||
|
Name string
|
||||||
|
PID int
|
||||||
|
Running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopProcess stops a process by PID
|
||||||
|
func StopProcess(pid int) error {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try graceful shutdown first (SIGTERM)
|
||||||
|
if err := process.Signal(syscall.SIGTERM); err != nil {
|
||||||
|
return fmt.Errorf("failed to send SIGTERM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -45,6 +45,7 @@ cleanup() {
|
|||||||
log_info "Cleaning up test environment..."
|
log_info "Cleaning up test environment..."
|
||||||
pkill -f "$BINARY" 2>/dev/null || true
|
pkill -f "$BINARY" 2>/dev/null || true
|
||||||
rm -rf "$TEST_DIR" 2>/dev/null || true
|
rm -rf "$TEST_DIR" 2>/dev/null || true
|
||||||
|
rm -rf "$HOME/.kvs" 2>/dev/null || true # Clean up PID and log files from home dir
|
||||||
sleep 2 # Allow processes to fully terminate
|
sleep 2 # Allow processes to fully terminate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ wait_for_service() {
|
|||||||
local port=$1
|
local port=$1
|
||||||
local timeout=${2:-30}
|
local timeout=${2:-30}
|
||||||
local count=0
|
local count=0
|
||||||
|
|
||||||
while [ $count -lt $timeout ]; do
|
while [ $count -lt $timeout ]; do
|
||||||
if curl -s "http://localhost:$port/health" >/dev/null 2>&1; then
|
if curl -s "http://localhost:$port/health" >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
@@ -64,6 +65,15 @@ wait_for_service() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get log file path for a config file (matches daemon naming convention)
|
||||||
|
get_log_file() {
|
||||||
|
local config=$1
|
||||||
|
local abs_path=$(realpath "$config")
|
||||||
|
local basename=$(basename "$config")
|
||||||
|
local dirname=$(basename $(dirname "$abs_path"))
|
||||||
|
echo "$HOME/.kvs/logs/${dirname}_${basename}.log"
|
||||||
|
}
|
||||||
|
|
||||||
# Test 1: Build verification
|
# Test 1: Build verification
|
||||||
test_build() {
|
test_build() {
|
||||||
test_start "Binary build verification"
|
test_start "Binary build verification"
|
||||||
@@ -82,7 +92,7 @@ test_build() {
|
|||||||
# Test 2: Basic functionality
|
# Test 2: Basic functionality
|
||||||
test_basic_functionality() {
|
test_basic_functionality() {
|
||||||
test_start "Basic functionality test"
|
test_start "Basic functionality test"
|
||||||
|
|
||||||
# Create basic config
|
# Create basic config
|
||||||
cat > basic.yaml <<EOF
|
cat > basic.yaml <<EOF
|
||||||
node_id: "basic-test"
|
node_id: "basic-test"
|
||||||
@@ -94,20 +104,20 @@ log_level: "error"
|
|||||||
allow_anonymous_read: true
|
allow_anonymous_read: true
|
||||||
allow_anonymous_write: true
|
allow_anonymous_write: true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start node
|
# Start node using daemon command
|
||||||
$BINARY basic.yaml >/dev/null 2>&1 &
|
$BINARY start basic.yaml >/dev/null 2>&1
|
||||||
local pid=$!
|
sleep 2
|
||||||
|
|
||||||
if wait_for_service 8090; then
|
if wait_for_service 8090; then
|
||||||
# Test basic CRUD
|
# Test basic CRUD
|
||||||
local put_result=$(curl -s -X PUT http://localhost:8090/kv/test/basic \
|
local put_result=$(curl -s -X PUT http://localhost:8090/kv/test/basic \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"message":"hello world"}')
|
-d '{"message":"hello world"}')
|
||||||
|
|
||||||
local get_result=$(curl -s http://localhost:8090/kv/test/basic)
|
local get_result=$(curl -s http://localhost:8090/kv/test/basic)
|
||||||
local message=$(echo "$get_result" | jq -r '.data.message' 2>/dev/null) # Adjusted jq path
|
local message=$(echo "$get_result" | jq -r '.data.message' 2>/dev/null)
|
||||||
|
|
||||||
if [ "$message" = "hello world" ]; then
|
if [ "$message" = "hello world" ]; then
|
||||||
log_success "Basic CRUD operations work"
|
log_success "Basic CRUD operations work"
|
||||||
else
|
else
|
||||||
@@ -116,9 +126,9 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "Basic test node failed to start"
|
log_error "Basic test node failed to start"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop basic.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test 3: Cluster formation
|
# Test 3: Cluster formation
|
||||||
@@ -160,23 +170,22 @@ allow_anonymous_write: true
|
|||||||
cluster_secret: "$CLUSTER_SECRET"
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start nodes
|
# Start nodes using daemon commands
|
||||||
$BINARY cluster1.yaml >/dev/null 2>&1 &
|
$BINARY start cluster1.yaml >/dev/null 2>&1
|
||||||
local pid1=$!
|
sleep 2
|
||||||
|
|
||||||
if ! wait_for_service 8101; then
|
if ! wait_for_service 8101; then
|
||||||
log_error "Cluster node 1 failed to start"
|
log_error "Cluster node 1 failed to start"
|
||||||
kill $pid1 2>/dev/null || true
|
$BINARY stop cluster1.yaml >/dev/null 2>&1
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 2 # Give node 1 a moment to fully initialize
|
$BINARY start cluster2.yaml >/dev/null 2>&1
|
||||||
$BINARY cluster2.yaml >/dev/null 2>&1 &
|
sleep 2
|
||||||
local pid2=$!
|
|
||||||
|
|
||||||
if ! wait_for_service 8102; then
|
if ! wait_for_service 8102; then
|
||||||
log_error "Cluster node 2 failed to start"
|
log_error "Cluster node 2 failed to start"
|
||||||
kill $pid1 $pid2 2>/dev/null || true
|
$BINARY stop cluster1.yaml cluster2.yaml >/dev/null 2>&1
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -224,9 +233,9 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "Cluster formation failed (N1 members: $node1_members, N2 members: $node2_members)"
|
log_error "Cluster formation failed (N1 members: $node1_members, N2 members: $node2_members)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid1 $pid2 2>/dev/null || true
|
$BINARY stop cluster1.yaml cluster2.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test 4: Conflict resolution (Merkle Tree based)
|
# Test 4: Conflict resolution (Merkle Tree based)
|
||||||
@@ -274,16 +283,15 @@ allow_anonymous_write: true
|
|||||||
cluster_secret: "$CLUSTER_SECRET"
|
cluster_secret: "$CLUSTER_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start nodes
|
# Start nodes using daemon commands
|
||||||
# Node 1 started first, making it "older" for tie-breaker if timestamps are equal
|
# Node 1 started first, making it "older" for tie-breaker if timestamps are equal
|
||||||
"$BINARY" conflict1.yaml >conflict1.log 2>&1 &
|
$BINARY start conflict1.yaml >/dev/null 2>&1
|
||||||
local pid1=$!
|
sleep 2
|
||||||
|
|
||||||
if wait_for_service 8111; then
|
if wait_for_service 8111; then
|
||||||
|
$BINARY start conflict2.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 2
|
||||||
$BINARY conflict2.yaml >conflict2.log 2>&1 &
|
|
||||||
local pid2=$!
|
|
||||||
|
|
||||||
if wait_for_service 8112; then
|
if wait_for_service 8112; then
|
||||||
# Get initial data (full StoredValue)
|
# Get initial data (full StoredValue)
|
||||||
local node1_initial_full=$(curl -s http://localhost:8111/kv/test/conflict/data)
|
local node1_initial_full=$(curl -s http://localhost:8111/kv/test/conflict/data)
|
||||||
@@ -344,8 +352,10 @@ EOF
|
|||||||
log_error "Resolved data has inconsistent UUID/Timestamp: N1_UUID=$node1_final_uuid, N1_TS=$node1_final_timestamp, N2_UUID=$node2_final_uuid, N2_TS=$node2_final_timestamp"
|
log_error "Resolved data has inconsistent UUID/Timestamp: N1_UUID=$node1_final_uuid, N1_TS=$node1_final_timestamp, N2_UUID=$node2_final_uuid, N2_TS=$node2_final_timestamp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Optionally, check logs for conflict resolution messages
|
# Check logs for conflict resolution messages
|
||||||
if grep -q "Conflict resolved" conflict1.log conflict2.log 2>/dev/null; then
|
local log1=$(get_log_file conflict1.yaml)
|
||||||
|
local log2=$(get_log_file conflict2.yaml)
|
||||||
|
if grep -q "Conflict resolved" "$log1" "$log2" 2>/dev/null; then
|
||||||
log_success "Conflict resolution messages found in logs"
|
log_success "Conflict resolution messages found in logs"
|
||||||
else
|
else
|
||||||
log_error "No 'Conflict resolved' messages found in logs, but data converged."
|
log_error "No 'Conflict resolved' messages found in logs, but data converged."
|
||||||
@@ -357,14 +367,14 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "Conflict node 2 failed to start"
|
log_error "Conflict node 2 failed to start"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid2 2>/dev/null || true
|
$BINARY stop conflict2.yaml >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
log_error "Conflict node 1 failed to start"
|
log_error "Conflict node 1 failed to start"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid1 2>/dev/null || true
|
$BINARY stop conflict1.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 1
|
||||||
else
|
else
|
||||||
cd "$TEST_DIR"
|
cd "$TEST_DIR"
|
||||||
log_error "Failed to create conflict test data. Ensure test_conflict.go is correct."
|
log_error "Failed to create conflict test data. Ensure test_conflict.go is correct."
|
||||||
@@ -388,22 +398,21 @@ allow_anonymous_read: false
|
|||||||
allow_anonymous_write: false
|
allow_anonymous_write: false
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start node
|
# Start node using daemon command
|
||||||
$BINARY auth_test.yaml >auth_test.log 2>&1 &
|
$BINARY start auth_test.yaml >/dev/null 2>&1
|
||||||
local pid=$!
|
sleep 3 # Allow daemon to start and root account creation
|
||||||
|
|
||||||
if wait_for_service 8095; then
|
if wait_for_service 8095; then
|
||||||
sleep 2 # Allow root account creation
|
|
||||||
|
|
||||||
# Extract the token from logs
|
# Extract the token from logs
|
||||||
local token=$(grep "Token:" auth_test.log | sed 's/.*Token: //' | tr -d '\n\r')
|
local log_file=$(get_log_file auth_test.yaml)
|
||||||
|
local token=$(grep "Token:" "$log_file" | sed 's/.*Token: //' | tr -d '\n\r')
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
if [ -z "$token" ]; then
|
||||||
log_error "Failed to extract authentication token from logs"
|
log_error "Failed to extract authentication token from logs"
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 1: Admin endpoints should fail without authentication
|
# Test 1: Admin endpoints should fail without authentication
|
||||||
local no_auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -d '{"nickname":"test","password":"test"}')
|
local no_auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -d '{"nickname":"test","password":"test"}')
|
||||||
if echo "$no_auth_response" | grep -q "Unauthorized"; then
|
if echo "$no_auth_response" | grep -q "Unauthorized"; then
|
||||||
@@ -411,7 +420,7 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "Admin endpoints should reject unauthenticated requests, got: $no_auth_response"
|
log_error "Admin endpoints should reject unauthenticated requests, got: $no_auth_response"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 2: Admin endpoints should work with valid authentication
|
# Test 2: Admin endpoints should work with valid authentication
|
||||||
local auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"nickname":"authtest","password":"authtest"}')
|
local auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"nickname":"authtest","password":"authtest"}')
|
||||||
if echo "$auth_response" | grep -q "uuid"; then
|
if echo "$auth_response" | grep -q "uuid"; then
|
||||||
@@ -419,7 +428,7 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "Admin endpoints should work with authentication, got: $auth_response"
|
log_error "Admin endpoints should work with authentication, got: $auth_response"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 3: KV endpoints should require auth when anonymous access is disabled
|
# Test 3: KV endpoints should require auth when anonymous access is disabled
|
||||||
local kv_no_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -d '{"test":"auth"}')
|
local kv_no_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -d '{"test":"auth"}')
|
||||||
if echo "$kv_no_auth" | grep -q "Unauthorized"; then
|
if echo "$kv_no_auth" | grep -q "Unauthorized"; then
|
||||||
@@ -427,7 +436,7 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "KV endpoints should require auth when anonymous access disabled, got: $kv_no_auth"
|
log_error "KV endpoints should require auth when anonymous access disabled, got: $kv_no_auth"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 4: KV endpoints should work with valid authentication
|
# Test 4: KV endpoints should work with valid authentication
|
||||||
local kv_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"test":"auth"}')
|
local kv_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"test":"auth"}')
|
||||||
if echo "$kv_auth" | grep -q "uuid\|timestamp" || [ -z "$kv_auth" ]; then
|
if echo "$kv_auth" | grep -q "uuid\|timestamp" || [ -z "$kv_auth" ]; then
|
||||||
@@ -435,12 +444,12 @@ EOF
|
|||||||
else
|
else
|
||||||
log_error "KV endpoints should work with authentication, got: $kv_auth"
|
log_error "KV endpoints should work with authentication, got: $kv_auth"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 1
|
||||||
else
|
else
|
||||||
log_error "Auth test node failed to start"
|
log_error "Auth test node failed to start"
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,19 +470,18 @@ allow_anonymous_read: false
|
|||||||
allow_anonymous_write: false
|
allow_anonymous_write: false
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start node
|
# Start node using daemon command
|
||||||
$BINARY metadata_test.yaml >metadata_test.log 2>&1 &
|
$BINARY start metadata_test.yaml >/dev/null 2>&1
|
||||||
local pid=$!
|
sleep 3 # Allow daemon to start and root account creation
|
||||||
|
|
||||||
if wait_for_service 8096; then
|
if wait_for_service 8096; then
|
||||||
sleep 2 # Allow root account creation
|
|
||||||
|
|
||||||
# Extract the token from logs
|
# Extract the token from logs
|
||||||
local token=$(grep "Token:" metadata_test.log | sed 's/.*Token: //' | tr -d '\n\r')
|
local log_file=$(get_log_file metadata_test.yaml)
|
||||||
|
local token=$(grep "Token:" "$log_file" | sed 's/.*Token: //' | tr -d '\n\r')
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
if [ -z "$token" ]; then
|
||||||
log_error "Failed to extract authentication token from logs"
|
log_error "Failed to extract authentication token from logs"
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -525,14 +533,78 @@ EOF
|
|||||||
log_error "Metadata endpoints should require authentication, got code: $no_auth_code"
|
log_error "Metadata endpoints should require authentication, got code: $no_auth_code"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||||
sleep 2
|
sleep 1
|
||||||
else
|
else
|
||||||
log_error "Metadata test node failed to start"
|
log_error "Metadata test node failed to start"
|
||||||
kill $pid 2>/dev/null || true
|
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Test 7: Daemon commands (start, stop, status, restart)
|
||||||
|
test_daemon_commands() {
|
||||||
|
test_start "Daemon command tests (start, stop, status, restart)"
|
||||||
|
|
||||||
|
# Create daemon test config
|
||||||
|
cat > daemon_test.yaml <<EOF
|
||||||
|
node_id: "daemon-test"
|
||||||
|
bind_address: "127.0.0.1"
|
||||||
|
port: 8097
|
||||||
|
data_dir: "./daemon_test_data"
|
||||||
|
seed_nodes: []
|
||||||
|
log_level: "error"
|
||||||
|
allow_anonymous_read: true
|
||||||
|
allow_anonymous_write: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Test 1: Start command
|
||||||
|
$BINARY start daemon_test.yaml >/dev/null 2>&1
|
||||||
|
sleep 3 # Allow daemon to start
|
||||||
|
|
||||||
|
if wait_for_service 8097 5; then
|
||||||
|
log_success "Daemon 'start' command works"
|
||||||
|
|
||||||
|
# Test 2: Status command shows running
|
||||||
|
local status_output=$($BINARY status daemon_test.yaml 2>&1)
|
||||||
|
if echo "$status_output" | grep -q "RUNNING"; then
|
||||||
|
log_success "Daemon 'status' command shows RUNNING"
|
||||||
|
else
|
||||||
|
log_error "Daemon 'status' should show RUNNING, got: $status_output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Stop command
|
||||||
|
$BINARY stop daemon_test.yaml >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check that service is actually stopped
|
||||||
|
if ! curl -s "http://localhost:8097/health" >/dev/null 2>&1; then
|
||||||
|
log_success "Daemon 'stop' command works"
|
||||||
|
else
|
||||||
|
log_error "Daemon should be stopped but is still responding"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Restart command
|
||||||
|
$BINARY restart daemon_test.yaml >/dev/null 2>&1
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if wait_for_service 8097 5; then
|
||||||
|
log_success "Daemon 'restart' command works"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
$BINARY stop daemon_test.yaml >/dev/null 2>&1
|
||||||
|
sleep 1
|
||||||
|
else
|
||||||
|
log_error "Daemon 'restart' failed to start service"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "Daemon 'start' command failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure cleanup
|
||||||
|
pkill -f "daemon_test.yaml" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
# Main test execution
|
# Main test execution
|
||||||
main() {
|
main() {
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
@@ -552,6 +624,7 @@ main() {
|
|||||||
test_conflict_resolution
|
test_conflict_resolution
|
||||||
test_authentication_middleware
|
test_authentication_middleware
|
||||||
test_metadata_management
|
test_metadata_management
|
||||||
|
test_daemon_commands
|
||||||
|
|
||||||
# Results
|
# Results
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
|
207
main.go
207
main.go
@@ -6,25 +6,90 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"kvs/config"
|
"kvs/config"
|
||||||
|
"kvs/daemon"
|
||||||
"kvs/server"
|
"kvs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configPath := "./config.yaml"
|
if len(os.Args) < 2 {
|
||||||
|
// No arguments - run in foreground with default config
|
||||||
// Simple CLI argument parsing
|
runServer("./config.yaml", false)
|
||||||
if len(os.Args) > 1 {
|
return
|
||||||
configPath = os.Args[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a daemon spawn
|
||||||
|
if os.Args[1] == "--daemon" {
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: --daemon flag requires config path\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
runServer(os.Args[2], true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse subcommand
|
||||||
|
command := os.Args[1]
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "start":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs start <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdStart(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs stop <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdStop(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs restart <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdRestart(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
cmdStatusSingle(normalizeConfigPath(os.Args[2]))
|
||||||
|
} else {
|
||||||
|
cmdStatusAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printHelp()
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Backward compatibility: assume it's a config file path
|
||||||
|
runServer(command, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServer(configPath string, isDaemon bool) {
|
||||||
cfg, err := config.Load(configPath)
|
cfg, err := config.Load(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write PID file if running as daemon
|
||||||
|
if isDaemon {
|
||||||
|
if err := daemon.WritePID(configPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write PID file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer daemon.RemovePID(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
kvServer, err := server.NewServer(cfg)
|
kvServer, err := server.NewServer(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
|
||||||
@@ -45,3 +110,135 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdStart(configPath string) {
|
||||||
|
if err := daemon.Daemonize(configPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to start: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStop(configPath string) {
|
||||||
|
pid, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read PID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
fmt.Printf("Instance '%s' is not running\n", configPath)
|
||||||
|
// Clean up stale PID file
|
||||||
|
daemon.RemovePID(configPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Stopping instance '%s' (PID %d)...\n", configPath, pid)
|
||||||
|
if err := daemon.StopProcess(pid); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to stop process: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit and verify it stopped
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
_, stillRunning, _ := daemon.ReadPID(configPath)
|
||||||
|
if stillRunning {
|
||||||
|
fmt.Printf("Warning: Process may still be running\n")
|
||||||
|
} else {
|
||||||
|
daemon.RemovePID(configPath)
|
||||||
|
fmt.Printf("Stopped successfully\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdRestart(configPath string) {
|
||||||
|
// Check if running
|
||||||
|
_, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to check status: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if running {
|
||||||
|
cmdStop(configPath)
|
||||||
|
// Wait a bit for clean shutdown
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdStart(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStatusSingle(configPath string) {
|
||||||
|
pid, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read PID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if running {
|
||||||
|
fmt.Printf("Instance '%s': RUNNING (PID %d)\n", configPath, pid)
|
||||||
|
} else if pid > 0 {
|
||||||
|
fmt.Printf("Instance '%s': STOPPED (stale PID %d)\n", configPath, pid)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Instance '%s': STOPPED\n", configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStatusAll() {
|
||||||
|
instances, err := daemon.ListRunningInstances()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to list instances: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instances) == 0 {
|
||||||
|
fmt.Println("No KVS instances found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("KVS Instances:")
|
||||||
|
for _, inst := range instances {
|
||||||
|
status := "STOPPED"
|
||||||
|
if inst.Running {
|
||||||
|
status = "RUNNING"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %-20s %s (PID %d)\n", inst.Name, status, inst.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeConfigPath ensures config path has .yaml extension if not specified
|
||||||
|
func normalizeConfigPath(path string) string {
|
||||||
|
// If path doesn't have an extension, add .yaml
|
||||||
|
if filepath.Ext(path) == "" {
|
||||||
|
return path + ".yaml"
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigIdentifier returns the identifier for a config (basename without extension)
|
||||||
|
// This is used for PID files and status display
|
||||||
|
func getConfigIdentifier(path string) string {
|
||||||
|
basename := filepath.Base(path)
|
||||||
|
return strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
help := `KVS - Distributed Key-Value Store
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
kvs [config.yaml] Run in foreground (default: ./config.yaml)
|
||||||
|
kvs start <config> Start as daemon (.yaml extension optional)
|
||||||
|
kvs stop <config> Stop daemon (.yaml extension optional)
|
||||||
|
kvs restart <config> Restart daemon (.yaml extension optional)
|
||||||
|
kvs status [config] Show status (all instances if no config given)
|
||||||
|
kvs help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
kvs # Run with ./config.yaml in foreground
|
||||||
|
kvs node1.yaml # Run with node1.yaml in foreground
|
||||||
|
kvs start node1 # Start node1.yaml as daemon
|
||||||
|
kvs start node1.yaml # Same as above
|
||||||
|
kvs stop node1 # Stop node1 daemon
|
||||||
|
kvs status # Show all running instances
|
||||||
|
kvs status node1 # Show status of node1
|
||||||
|
`
|
||||||
|
fmt.Print(help)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user