|
1 | 1 | # QuackQuack |
2 | 2 |
|
3 | | -QuackQuack is a Go library for managing DuckDB databases with support for periodic snapshots and restoration. |
| 3 | +QuackQuack is a resilient Go library for managing DuckDB databases with support for periodic snapshots, restoration, and comprehensive health monitoring. It provides robust error handling, graceful degradation, and detailed status reporting for production environments. |
4 | 4 |
|
5 | | -## Installation |
| 5 | +## Features |
| 6 | + |
| 7 | +- 🔄 **Automatic Snapshots**: Periodic database snapshots in Parquet format |
| 8 | +- 🔧 **Resilient Extension Loading**: Multiple fallback strategies for DuckDB extensions |
| 9 | +- 🏥 **Health Monitoring**: Comprehensive health checks and status reporting |
| 10 | +- ⚡ **Graceful Degradation**: Continues operation even when optional features fail |
| 11 | +- 🛡️ **Input Validation**: Early detection of configuration issues |
| 12 | +- 📊 **Detailed Logging**: Rich logging and error reporting |
| 13 | +- 🔍 **Extension Status Tracking**: Monitor which extensions loaded successfully |
6 | 14 |
|
7 | | -To install DuckDBStorage, use `go get`: |
| 15 | +## Installation |
8 | 16 |
|
9 | 17 | ```sh |
10 | 18 | go get github.com/OpenCHAMI/quack/quack |
11 | 19 | ``` |
12 | 20 |
|
13 | | -## Usage |
| 21 | +## Quick Start |
14 | 22 |
|
15 | | -Here's a basic example of how to use it: |
| 23 | +### Basic Usage |
16 | 24 |
|
17 | 25 | ```go |
18 | 26 | package main |
19 | 27 |
|
20 | 28 | import ( |
| 29 | + "context" |
21 | 30 | "log" |
22 | 31 | "time" |
23 | 32 |
|
24 | | - "github.com/OpenCHAMI/quack" |
| 33 | + "github.com/OpenCHAMI/quack/quack" |
25 | 34 | ) |
26 | 35 |
|
27 | 36 | func main() { |
28 | | - storage, err := quack.NewDuckDBStorage("path/to/db", quack.WithSnapshotFrequency(10*time.Minute)) |
| 37 | + // Create database with automatic snapshots |
| 38 | + storage, err := quack.NewDuckDBStorage("myapp.db", |
| 39 | + quack.WithSnapshotFrequency(10*time.Minute), |
| 40 | + quack.WithSnapshotPath("backups/"), |
| 41 | + quack.WithCreateSnapshotDir(true)) |
| 42 | + |
29 | 43 | if err != nil { |
30 | | - log.Fatalf("Failed to initialize DuckDBStorage: %v", err) |
| 44 | + log.Fatalf("Failed to initialize database: %v", err) |
31 | 45 | } |
32 | | - defer storage.Close() |
| 46 | + defer storage.Shutdown(context.Background()) |
33 | 47 |
|
34 | | - // Your code here |
| 48 | + // Check health status |
| 49 | + if !storage.IsHealthy() { |
| 50 | + health := storage.HealthCheck() |
| 51 | + for _, rec := range health.Recommendations { |
| 52 | + log.Printf("Recommendation: %s", rec) |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + // Use the database |
| 57 | + db := storage.DB() |
| 58 | + _, err = db.Exec("CREATE TABLE users (id INTEGER, name TEXT)") |
| 59 | + if err != nil { |
| 60 | + log.Printf("Error creating table: %v", err) |
| 61 | + } |
35 | 62 | } |
36 | 63 | ``` |
37 | 64 |
|
38 | | -For a more detailed example, check out the [example/](example/) directory. |
| 65 | +### Production Example with Monitoring |
| 66 | + |
| 67 | +```go |
| 68 | +package main |
| 69 | + |
| 70 | +import ( |
| 71 | + "context" |
| 72 | + "log" |
| 73 | + "time" |
| 74 | + |
| 75 | + "github.com/OpenCHAMI/quack/quack" |
| 76 | +) |
| 77 | + |
| 78 | +func main() { |
| 79 | + // Production configuration |
| 80 | + storage, err := quack.NewDuckDBStorage("production.db", |
| 81 | + quack.WithSnapshotFrequency(1*time.Hour), |
| 82 | + quack.WithSnapshotPath("/var/backups/myapp/"), |
| 83 | + quack.WithCreateSnapshotDir(true)) |
| 84 | + |
| 85 | + if err != nil { |
| 86 | + // Get detailed error information |
| 87 | + log.Fatalf("Database initialization failed: %v", err) |
| 88 | + } |
| 89 | + defer storage.Shutdown(context.Background()) |
| 90 | + |
| 91 | + // Check for any initialization issues |
| 92 | + if initErrors := storage.GetInitializationErrors(); len(initErrors) > 0 { |
| 93 | + for _, err := range initErrors { |
| 94 | + log.Printf("Initialization warning: %v", err) |
| 95 | + } |
| 96 | + } |
39 | 97 |
|
40 | | -## Configuration |
| 98 | + // Monitor extension status |
| 99 | + extStatus := storage.GetExtensionStatus() |
| 100 | + log.Printf("Extensions loaded: %v", extStatus.Loaded) |
| 101 | + if len(extStatus.Failed) > 0 { |
| 102 | + log.Printf("Extensions failed: %v", extStatus.Failed) |
| 103 | + } |
41 | 104 |
|
42 | | -DuckDBStorage supports several configuration options through functional options: |
43 | | -* WithSnapshotFrequency(duration time.Duration): Sets the frequency of database snapshots. |
44 | | -* WithSnapshotPath(path string): Sets the path where snapshots will be stored. |
45 | | -* WithRestoreFirst(restore bool): If set to true, the database will be restored from the latest snapshot on initialization. |
46 | | -Example: |
| 105 | + // Periodic health monitoring |
| 106 | + go func() { |
| 107 | + ticker := time.NewTicker(5 * time.Minute) |
| 108 | + defer ticker.Stop() |
| 109 | + |
| 110 | + for range ticker.C { |
| 111 | + health := storage.HealthCheck() |
| 112 | + if !health.Healthy { |
| 113 | + log.Printf("Health check failed: database_ok=%v", health.DatabaseOK) |
| 114 | + for _, rec := range health.Recommendations { |
| 115 | + log.Printf("Recommendation: %s", rec) |
| 116 | + } |
| 117 | + } else { |
| 118 | + log.Printf("Health check passed") |
| 119 | + } |
| 120 | + } |
| 121 | + }() |
| 122 | + |
| 123 | + // Your application logic here |
| 124 | + db := storage.DB() |
| 125 | + // ... use database |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +## Configuration Options |
| 130 | + |
| 131 | +All configuration is done through functional options: |
| 132 | + |
| 133 | +### Core Options |
| 134 | + |
| 135 | +| Option | Description | Example | |
| 136 | +|--------|-------------|---------| |
| 137 | +| `WithSnapshotFrequency(duration)` | Enable automatic snapshots at specified interval | `WithSnapshotFrequency(1*time.Hour)` | |
| 138 | +| `WithSnapshotPath(path)` | Set snapshot storage directory | `WithSnapshotPath("/backups/")` | |
| 139 | +| `WithCreateSnapshotDir(bool)` | Auto-create snapshot directory if missing | `WithCreateSnapshotDir(true)` | |
| 140 | +| `WithRestore(path)` | Restore from snapshot on startup | `WithRestore("/backups/latest/")` | |
| 141 | + |
| 142 | +### Extension Management |
| 143 | + |
| 144 | +| Option | Description | Example | |
| 145 | +|--------|-------------|---------| |
| 146 | +| `WithSkipExtensions(bool)` | Skip all extension loading | `WithSkipExtensions(true)` | |
| 147 | + |
| 148 | +You can also use the environment variable `DUCKDB_SKIP_EXTENSIONS=true` to disable extensions. |
| 149 | + |
| 150 | +### Advanced Example |
47 | 151 |
|
48 | 152 | ```go |
49 | | -storage, err := quack.NewDuckDBStorage( |
50 | | - "path/to/db", |
51 | | - quack.WithSnapshotFrequency(10*time.Minute), |
52 | | - quack.WithSnapshotPath("path/to/snapshots"), |
53 | | - quack.WithRestoreFirst(true), |
| 153 | +storage, err := quack.NewDuckDBStorage("app.db", |
| 154 | + // Snapshot configuration |
| 155 | + quack.WithSnapshotFrequency(30*time.Minute), |
| 156 | + quack.WithSnapshotPath("./backups/"), |
| 157 | + quack.WithCreateSnapshotDir(true), |
| 158 | + |
| 159 | + // Extension configuration (useful in containerized environments) |
| 160 | + quack.WithSkipExtensions(false), // Default: try to load extensions |
54 | 161 | ) |
55 | 162 | ``` |
56 | 163 |
|
57 | | -License |
| 164 | +## Health Monitoring |
| 165 | + |
| 166 | +QuackQuack provides comprehensive health monitoring: |
| 167 | + |
| 168 | +```go |
| 169 | +// Quick health check |
| 170 | +if storage.IsHealthy() { |
| 171 | + log.Println("Database is healthy") |
| 172 | +} |
| 173 | + |
| 174 | +// Detailed health information |
| 175 | +health := storage.HealthCheck() |
| 176 | +fmt.Printf("Database OK: %v\n", health.DatabaseOK) |
| 177 | +fmt.Printf("Extensions Loaded: %v\n", health.ExtensionStatus.Loaded) |
| 178 | +fmt.Printf("Extensions Failed: %v\n", health.ExtensionStatus.Failed) |
| 179 | + |
| 180 | +// Get actionable recommendations |
| 181 | +for _, rec := range health.Recommendations { |
| 182 | + fmt.Printf("💡 %s\n", rec) |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### Health Status Structure |
| 187 | + |
| 188 | +```go |
| 189 | +type HealthStatus struct { |
| 190 | + Healthy bool `json:"healthy"` |
| 191 | + DatabaseOK bool `json:"database_ok"` |
| 192 | + ExtensionStatus ExtensionStatus `json:"extension_status"` |
| 193 | + SnapshotEnabled bool `json:"snapshot_enabled"` |
| 194 | + InitErrors []string `json:"init_errors,omitempty"` |
| 195 | + LastHealthCheck time.Time `json:"last_health_check"` |
| 196 | + Recommendations []string `json:"recommendations,omitempty"` |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +## Error Handling |
| 201 | + |
| 202 | +QuackQuack provides detailed error information and validation: |
| 203 | + |
| 204 | +```go |
| 205 | +storage, err := quack.NewDuckDBStorage("invalid\x00path.db") |
| 206 | +if err != nil { |
| 207 | + // Will get: "validation error for path='invalid\x00path.db': database path contains null bytes" |
| 208 | + log.Printf("Validation failed: %v", err) |
| 209 | +} |
| 210 | + |
| 211 | +// Check for non-fatal initialization issues |
| 212 | +if initErrors := storage.GetInitializationErrors(); len(initErrors) > 0 { |
| 213 | + for _, err := range initErrors { |
| 214 | + log.Printf("Warning: %v", err) |
| 215 | + } |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +## Extension Management |
| 220 | + |
| 221 | +QuackQuack uses a multi-strategy approach for loading DuckDB extensions: |
| 222 | + |
| 223 | +1. **Local Loading**: Try to load pre-installed extensions |
| 224 | +2. **Auto-Install**: Download and install extensions automatically |
| 225 | +3. **Basic Fallback**: Minimal extension loading |
| 226 | + |
| 227 | +```go |
| 228 | +// Check extension status |
| 229 | +extStatus := storage.GetExtensionStatus() |
| 230 | +fmt.Printf("Strategy used: %d\n", extStatus.Strategy) |
| 231 | +fmt.Printf("Loaded: %v\n", extStatus.Loaded) |
| 232 | +fmt.Printf("Failed: %v\n", extStatus.Failed) |
| 233 | +fmt.Printf("Skipped: %v\n", extStatus.Skipped) |
| 234 | + |
| 235 | +// Disable extensions in problematic environments |
| 236 | +storage, err := quack.NewDuckDBStorage("app.db", |
| 237 | + quack.WithSkipExtensions(true)) |
| 238 | + |
| 239 | +// Or via environment variable |
| 240 | +os.Setenv("DUCKDB_SKIP_EXTENSIONS", "true") |
| 241 | +``` |
| 242 | + |
| 243 | +## Environment Variables |
| 244 | + |
| 245 | +| Variable | Description | Default | |
| 246 | +|----------|-------------|---------| |
| 247 | +| `DUCKDB_HOME` | Directory for extension storage | `$HOME` | |
| 248 | +| `DUCKDB_SKIP_EXTENSIONS` | Skip extension loading (`true`/`false`) | `false` | |
| 249 | + |
| 250 | +## Snapshots and Restoration |
| 251 | + |
| 252 | +### Manual Snapshots |
| 253 | + |
| 254 | +```go |
| 255 | +ctx := context.Background() |
| 256 | +err := storage.SnapshotParquet(ctx, "./manual-backup/") |
| 257 | +if err != nil { |
| 258 | + log.Printf("Snapshot failed: %v", err) |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +### Restoration |
| 263 | + |
| 264 | +```go |
| 265 | +// Restore from specific snapshot |
| 266 | +storage, err := quack.NewDuckDBStorage("restored.db", |
| 267 | + quack.WithRestore("./backups/2023-01-01T12-00-00/")) |
| 268 | +``` |
| 269 | + |
| 270 | +## Troubleshooting |
| 271 | + |
| 272 | +### Common Issues |
| 273 | + |
| 274 | +1. **Extension Loading Failures**: |
| 275 | + ``` |
| 276 | + Set DUCKDB_SKIP_EXTENSIONS=true if extensions aren't needed |
| 277 | + ``` |
| 278 | + |
| 279 | +2. **Permission Errors**: |
| 280 | + ``` |
| 281 | + Ensure the database directory is writable |
| 282 | + Check DUCKDB_HOME permissions |
| 283 | + ``` |
| 284 | + |
| 285 | +3. **Snapshot Directory Issues**: |
| 286 | + ``` |
| 287 | + Use WithCreateSnapshotDir(true) to auto-create directories |
| 288 | + ``` |
| 289 | + |
| 290 | +### Getting Help |
| 291 | + |
| 292 | +Check the health status for actionable recommendations: |
| 293 | + |
| 294 | +```go |
| 295 | +health := storage.HealthCheck() |
| 296 | +for _, rec := range health.Recommendations { |
| 297 | + fmt.Printf("💡 %s\n", rec) |
| 298 | +} |
| 299 | +``` |
| 300 | +} |
| 301 | +``` |
| 302 | +
|
| 303 | +## Examples |
| 304 | +
|
| 305 | +For a complete working example, check out the [example/](example/) directory. |
| 306 | +
|
| 307 | +## License |
| 308 | +
|
58 | 309 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. |
0 commit comments