Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0a401cab1 | |||
| 59a717abe7 | |||
| 490a12f858 | |||
| ea4337e298 | |||
| bbd4f0ceac | |||
| f6f8b04785 | |||
| 670c9af2e7 | |||
| e2cf9adc62 | |||
| 29e089fe3b | |||
| 9396c8e605 | |||
| e363e1937f | |||
| df1ab2f55b |
42
README.md
42
README.md
@@ -194,21 +194,51 @@ r: Restore | v: Verify | i: Info | d: Diagnose | D: Delete | R: Refresh | Esc: B
|
|||||||
```
|
```
|
||||||
Configuration Settings
|
Configuration Settings
|
||||||
|
|
||||||
|
[SYSTEM] Detected Resources
|
||||||
|
CPU: 8 physical cores, 16 logical cores
|
||||||
|
Memory: 32GB total, 28GB available
|
||||||
|
Recommended Profile: balanced
|
||||||
|
→ 8 cores and 32GB RAM supports moderate parallelism
|
||||||
|
|
||||||
|
[CONFIG] Current Settings
|
||||||
|
Target DB: PostgreSQL (postgres)
|
||||||
|
Database: postgres@localhost:5432
|
||||||
|
Backup Dir: /var/backups/postgres
|
||||||
|
Compression: Level 6
|
||||||
|
Profile: balanced | Cluster: 2 parallel | Jobs: 4
|
||||||
|
|
||||||
> Database Type: postgres
|
> Database Type: postgres
|
||||||
CPU Workload Type: balanced
|
CPU Workload Type: balanced
|
||||||
Backup Directory: /root/db_backups
|
Resource Profile: balanced (P:2 J:4)
|
||||||
Work Directory: /tmp
|
Cluster Parallelism: 2
|
||||||
|
Backup Directory: /var/backups/postgres
|
||||||
|
Work Directory: (system temp)
|
||||||
Compression Level: 6
|
Compression Level: 6
|
||||||
Parallel Jobs: 16
|
Parallel Jobs: 4
|
||||||
Dump Jobs: 8
|
Dump Jobs: 4
|
||||||
Database Host: localhost
|
Database Host: localhost
|
||||||
Database Port: 5432
|
Database Port: 5432
|
||||||
Database User: root
|
Database User: postgres
|
||||||
SSL Mode: prefer
|
SSL Mode: prefer
|
||||||
|
|
||||||
s: Save | r: Reset | q: Menu
|
[KEYS] ↑↓ navigate | Enter edit | 'l' toggle LargeDB | 'c' conservative | 'p' recommend | 's' save | 'q' menu
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Resource Profiles for Large Databases:**
|
||||||
|
|
||||||
|
When restoring large databases on VMs with limited resources, use the resource profile settings to prevent "out of shared memory" errors:
|
||||||
|
|
||||||
|
| Profile | Cluster Parallel | Jobs | Best For |
|
||||||
|
|---------|------------------|------|----------|
|
||||||
|
| conservative | 1 | 1 | Small VMs (<16GB RAM) |
|
||||||
|
| balanced | 2 | 2-4 | Medium VMs (16-32GB RAM) |
|
||||||
|
| performance | 4 | 4-8 | Large servers (32GB+ RAM) |
|
||||||
|
| max-performance | 8 | 8-16 | High-end servers (64GB+) |
|
||||||
|
|
||||||
|
**Large DB Mode:** Toggle with `l` key. Reduces parallelism by 50% and sets max_locks_per_transaction=8192 for complex databases with many tables/LOBs.
|
||||||
|
|
||||||
|
**Quick shortcuts:** Press `l` to toggle Large DB Mode, `c` for conservative, `p` to show recommendation.
|
||||||
|
|
||||||
**Database Status:**
|
**Database Status:**
|
||||||
```
|
```
|
||||||
Database Status & Health Check
|
Database Status & Health Check
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
|
|||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.50
|
- **Version**: 3.42.50
|
||||||
- **Build Time**: 2026-01-17_12:41:47_UTC
|
- **Build Time**: 2026-01-18_11:39:42_UTC
|
||||||
- **Git Commit**: 62d58c7
|
- **Git Commit**: 59a717a
|
||||||
|
|
||||||
## Recent Updates (v1.1.0)
|
## Recent Updates (v1.1.0)
|
||||||
- ✅ Fixed TUI progress display with line-by-line output
|
- ✅ Fixed TUI progress display with line-by-line output
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ type Config struct {
|
|||||||
AutoDetectCores bool
|
AutoDetectCores bool
|
||||||
CPUWorkloadType string // "cpu-intensive", "io-intensive", "balanced"
|
CPUWorkloadType string // "cpu-intensive", "io-intensive", "balanced"
|
||||||
|
|
||||||
|
// Resource profile for backup/restore operations
|
||||||
|
ResourceProfile string // "conservative", "balanced", "performance", "max-performance"
|
||||||
|
LargeDBMode bool // Enable large database mode (reduces parallelism, increases max_locks)
|
||||||
|
|
||||||
// CPU detection
|
// CPU detection
|
||||||
CPUDetector *cpu.Detector
|
CPUDetector *cpu.Detector
|
||||||
CPUInfo *cpu.CPUInfo
|
CPUInfo *cpu.CPUInfo
|
||||||
|
MemoryInfo *cpu.MemoryInfo // System memory information
|
||||||
|
|
||||||
// Sample backup options
|
// Sample backup options
|
||||||
SampleStrategy string // "ratio", "percent", "count"
|
SampleStrategy string // "ratio", "percent", "count"
|
||||||
@@ -178,6 +183,13 @@ func New() *Config {
|
|||||||
sslMode = ""
|
sslMode = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect memory information
|
||||||
|
memInfo, _ := cpu.DetectMemory()
|
||||||
|
|
||||||
|
// Determine recommended resource profile
|
||||||
|
recommendedProfile := cpu.RecommendProfile(cpuInfo, memInfo, false)
|
||||||
|
defaultProfile := getEnvString("RESOURCE_PROFILE", recommendedProfile.Name)
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
// Database defaults
|
// Database defaults
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -189,18 +201,21 @@ func New() *Config {
|
|||||||
SSLMode: sslMode,
|
SSLMode: sslMode,
|
||||||
Insecure: getEnvBool("INSECURE", false),
|
Insecure: getEnvBool("INSECURE", false),
|
||||||
|
|
||||||
// Backup defaults
|
// Backup defaults - use recommended profile's settings for small VMs
|
||||||
BackupDir: backupDir,
|
BackupDir: backupDir,
|
||||||
CompressionLevel: getEnvInt("COMPRESS_LEVEL", 6),
|
CompressionLevel: getEnvInt("COMPRESS_LEVEL", 6),
|
||||||
Jobs: getEnvInt("JOBS", getDefaultJobs(cpuInfo)),
|
Jobs: getEnvInt("JOBS", recommendedProfile.Jobs),
|
||||||
DumpJobs: getEnvInt("DUMP_JOBS", getDefaultDumpJobs(cpuInfo)),
|
DumpJobs: getEnvInt("DUMP_JOBS", recommendedProfile.DumpJobs),
|
||||||
MaxCores: getEnvInt("MAX_CORES", getDefaultMaxCores(cpuInfo)),
|
MaxCores: getEnvInt("MAX_CORES", getDefaultMaxCores(cpuInfo)),
|
||||||
AutoDetectCores: getEnvBool("AUTO_DETECT_CORES", true),
|
AutoDetectCores: getEnvBool("AUTO_DETECT_CORES", true),
|
||||||
CPUWorkloadType: getEnvString("CPU_WORKLOAD_TYPE", "balanced"),
|
CPUWorkloadType: getEnvString("CPU_WORKLOAD_TYPE", "balanced"),
|
||||||
|
ResourceProfile: defaultProfile,
|
||||||
|
LargeDBMode: getEnvBool("LARGE_DB_MODE", false),
|
||||||
|
|
||||||
// CPU detection
|
// CPU and memory detection
|
||||||
CPUDetector: cpuDetector,
|
CPUDetector: cpuDetector,
|
||||||
CPUInfo: cpuInfo,
|
CPUInfo: cpuInfo,
|
||||||
|
MemoryInfo: memInfo,
|
||||||
|
|
||||||
// Sample backup defaults
|
// Sample backup defaults
|
||||||
SampleStrategy: getEnvString("SAMPLE_STRATEGY", "ratio"),
|
SampleStrategy: getEnvString("SAMPLE_STRATEGY", "ratio"),
|
||||||
@@ -220,8 +235,8 @@ func New() *Config {
|
|||||||
// Timeouts - default 24 hours (1440 min) to handle very large databases with large objects
|
// Timeouts - default 24 hours (1440 min) to handle very large databases with large objects
|
||||||
ClusterTimeoutMinutes: getEnvInt("CLUSTER_TIMEOUT_MIN", 1440),
|
ClusterTimeoutMinutes: getEnvInt("CLUSTER_TIMEOUT_MIN", 1440),
|
||||||
|
|
||||||
// Cluster parallelism (default: 2 concurrent operations for faster cluster backup/restore)
|
// Cluster parallelism - use recommended profile's setting for small VMs
|
||||||
ClusterParallelism: getEnvInt("CLUSTER_PARALLELISM", 2),
|
ClusterParallelism: getEnvInt("CLUSTER_PARALLELISM", recommendedProfile.ClusterParallelism),
|
||||||
|
|
||||||
// Working directory for large operations (default: system temp)
|
// Working directory for large operations (default: system temp)
|
||||||
WorkDir: getEnvString("WORK_DIR", ""),
|
WorkDir: getEnvString("WORK_DIR", ""),
|
||||||
@@ -409,6 +424,56 @@ func (c *Config) OptimizeForCPU() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyResourceProfile applies a resource profile to the configuration
|
||||||
|
// This adjusts parallelism settings based on the chosen profile
|
||||||
|
func (c *Config) ApplyResourceProfile(profileName string) error {
|
||||||
|
profile := cpu.GetProfileByName(profileName)
|
||||||
|
if profile == nil {
|
||||||
|
return &ConfigError{
|
||||||
|
Field: "resource_profile",
|
||||||
|
Value: profileName,
|
||||||
|
Message: "unknown profile. Valid profiles: conservative, balanced, performance, max-performance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate profile against current system
|
||||||
|
isValid, warnings := cpu.ValidateProfileForSystem(profile, c.CPUInfo, c.MemoryInfo)
|
||||||
|
if !isValid {
|
||||||
|
// Log warnings but don't block - user may know what they're doing
|
||||||
|
_ = warnings // In production, log these warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply profile settings
|
||||||
|
c.ResourceProfile = profile.Name
|
||||||
|
c.ClusterParallelism = profile.ClusterParallelism
|
||||||
|
c.Jobs = profile.Jobs
|
||||||
|
c.DumpJobs = profile.DumpJobs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceProfileRecommendation returns the recommended profile and reason
|
||||||
|
func (c *Config) GetResourceProfileRecommendation(isLargeDB bool) (string, string) {
|
||||||
|
profile, reason := cpu.RecommendProfileWithReason(c.CPUInfo, c.MemoryInfo, isLargeDB)
|
||||||
|
return profile.Name, reason
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentProfile returns the current resource profile details
|
||||||
|
// If LargeDBMode is enabled, returns a modified profile with reduced parallelism
|
||||||
|
func (c *Config) GetCurrentProfile() *cpu.ResourceProfile {
|
||||||
|
profile := cpu.GetProfileByName(c.ResourceProfile)
|
||||||
|
if profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply LargeDBMode modifier if enabled
|
||||||
|
if c.LargeDBMode {
|
||||||
|
return cpu.ApplyLargeDBMode(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
// GetCPUInfo returns CPU information, detecting if necessary
|
// GetCPUInfo returns CPU information, detecting if necessary
|
||||||
func (c *Config) GetCPUInfo() (*cpu.CPUInfo, error) {
|
func (c *Config) GetCPUInfo() (*cpu.CPUInfo, error) {
|
||||||
if c.CPUInfo != nil {
|
if c.CPUInfo != nil {
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ type LocalConfig struct {
|
|||||||
DumpJobs int
|
DumpJobs int
|
||||||
|
|
||||||
// Performance settings
|
// Performance settings
|
||||||
CPUWorkload string
|
CPUWorkload string
|
||||||
MaxCores int
|
MaxCores int
|
||||||
ClusterTimeout int // Cluster operation timeout in minutes (default: 1440 = 24 hours)
|
ClusterTimeout int // Cluster operation timeout in minutes (default: 1440 = 24 hours)
|
||||||
|
ResourceProfile string
|
||||||
|
LargeDBMode bool // Enable large database mode (reduces parallelism, increases locks)
|
||||||
|
|
||||||
// Security settings
|
// Security settings
|
||||||
RetentionDays int
|
RetentionDays int
|
||||||
@@ -126,6 +128,10 @@ func LoadLocalConfig() (*LocalConfig, error) {
|
|||||||
if ct, err := strconv.Atoi(value); err == nil {
|
if ct, err := strconv.Atoi(value); err == nil {
|
||||||
cfg.ClusterTimeout = ct
|
cfg.ClusterTimeout = ct
|
||||||
}
|
}
|
||||||
|
case "resource_profile":
|
||||||
|
cfg.ResourceProfile = value
|
||||||
|
case "large_db_mode":
|
||||||
|
cfg.LargeDBMode = value == "true" || value == "1"
|
||||||
}
|
}
|
||||||
case "security":
|
case "security":
|
||||||
switch key {
|
switch key {
|
||||||
@@ -207,6 +213,12 @@ func SaveLocalConfig(cfg *LocalConfig) error {
|
|||||||
if cfg.ClusterTimeout != 0 {
|
if cfg.ClusterTimeout != 0 {
|
||||||
sb.WriteString(fmt.Sprintf("cluster_timeout = %d\n", cfg.ClusterTimeout))
|
sb.WriteString(fmt.Sprintf("cluster_timeout = %d\n", cfg.ClusterTimeout))
|
||||||
}
|
}
|
||||||
|
if cfg.ResourceProfile != "" {
|
||||||
|
sb.WriteString(fmt.Sprintf("resource_profile = %s\n", cfg.ResourceProfile))
|
||||||
|
}
|
||||||
|
if cfg.LargeDBMode {
|
||||||
|
sb.WriteString("large_db_mode = true\n")
|
||||||
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Security section
|
// Security section
|
||||||
@@ -280,6 +292,14 @@ func ApplyLocalConfig(cfg *Config, local *LocalConfig) {
|
|||||||
if local.ClusterTimeout != 0 {
|
if local.ClusterTimeout != 0 {
|
||||||
cfg.ClusterTimeoutMinutes = local.ClusterTimeout
|
cfg.ClusterTimeoutMinutes = local.ClusterTimeout
|
||||||
}
|
}
|
||||||
|
// Apply resource profile settings
|
||||||
|
if local.ResourceProfile != "" {
|
||||||
|
cfg.ResourceProfile = local.ResourceProfile
|
||||||
|
}
|
||||||
|
// LargeDBMode is a boolean - apply if true in config
|
||||||
|
if local.LargeDBMode {
|
||||||
|
cfg.LargeDBMode = true
|
||||||
|
}
|
||||||
if cfg.RetentionDays == 30 && local.RetentionDays != 0 {
|
if cfg.RetentionDays == 30 && local.RetentionDays != 0 {
|
||||||
cfg.RetentionDays = local.RetentionDays
|
cfg.RetentionDays = local.RetentionDays
|
||||||
}
|
}
|
||||||
@@ -294,22 +314,24 @@ func ApplyLocalConfig(cfg *Config, local *LocalConfig) {
|
|||||||
// ConfigFromConfig creates a LocalConfig from a Config
|
// ConfigFromConfig creates a LocalConfig from a Config
|
||||||
func ConfigFromConfig(cfg *Config) *LocalConfig {
|
func ConfigFromConfig(cfg *Config) *LocalConfig {
|
||||||
return &LocalConfig{
|
return &LocalConfig{
|
||||||
DBType: cfg.DatabaseType,
|
DBType: cfg.DatabaseType,
|
||||||
Host: cfg.Host,
|
Host: cfg.Host,
|
||||||
Port: cfg.Port,
|
Port: cfg.Port,
|
||||||
User: cfg.User,
|
User: cfg.User,
|
||||||
Database: cfg.Database,
|
Database: cfg.Database,
|
||||||
SSLMode: cfg.SSLMode,
|
SSLMode: cfg.SSLMode,
|
||||||
BackupDir: cfg.BackupDir,
|
BackupDir: cfg.BackupDir,
|
||||||
WorkDir: cfg.WorkDir,
|
WorkDir: cfg.WorkDir,
|
||||||
Compression: cfg.CompressionLevel,
|
Compression: cfg.CompressionLevel,
|
||||||
Jobs: cfg.Jobs,
|
Jobs: cfg.Jobs,
|
||||||
DumpJobs: cfg.DumpJobs,
|
DumpJobs: cfg.DumpJobs,
|
||||||
CPUWorkload: cfg.CPUWorkloadType,
|
CPUWorkload: cfg.CPUWorkloadType,
|
||||||
MaxCores: cfg.MaxCores,
|
MaxCores: cfg.MaxCores,
|
||||||
ClusterTimeout: cfg.ClusterTimeoutMinutes,
|
ClusterTimeout: cfg.ClusterTimeoutMinutes,
|
||||||
RetentionDays: cfg.RetentionDays,
|
ResourceProfile: cfg.ResourceProfile,
|
||||||
MinBackups: cfg.MinBackups,
|
LargeDBMode: cfg.LargeDBMode,
|
||||||
MaxRetries: cfg.MaxRetries,
|
RetentionDays: cfg.RetentionDays,
|
||||||
|
MinBackups: cfg.MinBackups,
|
||||||
|
MaxRetries: cfg.MaxRetries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
475
internal/cpu/profiles.go
Normal file
475
internal/cpu/profiles.go
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
package cpu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MemoryInfo holds system memory information
|
||||||
|
type MemoryInfo struct {
|
||||||
|
TotalBytes int64 `json:"total_bytes"`
|
||||||
|
AvailableBytes int64 `json:"available_bytes"`
|
||||||
|
FreeBytes int64 `json:"free_bytes"`
|
||||||
|
UsedBytes int64 `json:"used_bytes"`
|
||||||
|
SwapTotalBytes int64 `json:"swap_total_bytes"`
|
||||||
|
SwapFreeBytes int64 `json:"swap_free_bytes"`
|
||||||
|
TotalGB int `json:"total_gb"`
|
||||||
|
AvailableGB int `json:"available_gb"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceProfile defines a resource allocation profile for backup/restore operations
|
||||||
|
type ResourceProfile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ClusterParallelism int `json:"cluster_parallelism"` // Concurrent databases
|
||||||
|
Jobs int `json:"jobs"` // Parallel jobs within pg_restore
|
||||||
|
DumpJobs int `json:"dump_jobs"` // Parallel jobs for pg_dump
|
||||||
|
MaintenanceWorkMem string `json:"maintenance_work_mem"` // PostgreSQL recommendation
|
||||||
|
MaxLocksPerTxn int `json:"max_locks_per_txn"` // PostgreSQL recommendation
|
||||||
|
RecommendedForLarge bool `json:"recommended_for_large"` // Suitable for large DBs?
|
||||||
|
MinMemoryGB int `json:"min_memory_gb"` // Minimum memory for this profile
|
||||||
|
MinCores int `json:"min_cores"` // Minimum cores for this profile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined resource profiles
|
||||||
|
var (
|
||||||
|
// ProfileConservative - Safe for constrained VMs, avoids shared memory issues
|
||||||
|
ProfileConservative = ResourceProfile{
|
||||||
|
Name: "conservative",
|
||||||
|
Description: "Safe for small VMs (2-4 cores, <16GB). Sequential operations, minimal memory pressure. Best for large DBs on limited hardware.",
|
||||||
|
ClusterParallelism: 1,
|
||||||
|
Jobs: 1,
|
||||||
|
DumpJobs: 2,
|
||||||
|
MaintenanceWorkMem: "256MB",
|
||||||
|
MaxLocksPerTxn: 4096,
|
||||||
|
RecommendedForLarge: true,
|
||||||
|
MinMemoryGB: 4,
|
||||||
|
MinCores: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileBalanced - Default profile, works for most scenarios
|
||||||
|
ProfileBalanced = ResourceProfile{
|
||||||
|
Name: "balanced",
|
||||||
|
Description: "Balanced for medium VMs (4-8 cores, 16-32GB). Moderate parallelism with good safety margin.",
|
||||||
|
ClusterParallelism: 2,
|
||||||
|
Jobs: 2,
|
||||||
|
DumpJobs: 4,
|
||||||
|
MaintenanceWorkMem: "512MB",
|
||||||
|
MaxLocksPerTxn: 2048,
|
||||||
|
RecommendedForLarge: true,
|
||||||
|
MinMemoryGB: 16,
|
||||||
|
MinCores: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfilePerformance - Aggressive parallelism for powerful servers
|
||||||
|
ProfilePerformance = ResourceProfile{
|
||||||
|
Name: "performance",
|
||||||
|
Description: "Aggressive for powerful servers (8+ cores, 32GB+). Maximum parallelism for fast operations.",
|
||||||
|
ClusterParallelism: 4,
|
||||||
|
Jobs: 4,
|
||||||
|
DumpJobs: 8,
|
||||||
|
MaintenanceWorkMem: "1GB",
|
||||||
|
MaxLocksPerTxn: 1024,
|
||||||
|
RecommendedForLarge: false, // Large DBs may still need conservative
|
||||||
|
MinMemoryGB: 32,
|
||||||
|
MinCores: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileMaxPerformance - Maximum parallelism for high-end servers
|
||||||
|
ProfileMaxPerformance = ResourceProfile{
|
||||||
|
Name: "max-performance",
|
||||||
|
Description: "Maximum for high-end servers (16+ cores, 64GB+). Full CPU utilization.",
|
||||||
|
ClusterParallelism: 8,
|
||||||
|
Jobs: 8,
|
||||||
|
DumpJobs: 16,
|
||||||
|
MaintenanceWorkMem: "2GB",
|
||||||
|
MaxLocksPerTxn: 512,
|
||||||
|
RecommendedForLarge: false, // Large DBs should use LargeDBMode
|
||||||
|
MinMemoryGB: 64,
|
||||||
|
MinCores: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllProfiles contains all available profiles (VM resource-based)
|
||||||
|
AllProfiles = []ResourceProfile{
|
||||||
|
ProfileConservative,
|
||||||
|
ProfileBalanced,
|
||||||
|
ProfilePerformance,
|
||||||
|
ProfileMaxPerformance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetProfileByName returns a profile by its name
|
||||||
|
func GetProfileByName(name string) *ResourceProfile {
|
||||||
|
for _, p := range AllProfiles {
|
||||||
|
if strings.EqualFold(p.Name, name) {
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyLargeDBMode modifies a profile for large database operations.
|
||||||
|
// This is a modifier that reduces parallelism and increases max_locks_per_transaction
|
||||||
|
// to prevent "out of shared memory" errors with large databases (many tables, LOBs, etc.).
|
||||||
|
// It returns a new profile with adjusted settings, leaving the original unchanged.
|
||||||
|
func ApplyLargeDBMode(profile *ResourceProfile) *ResourceProfile {
|
||||||
|
if profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy with adjusted settings
|
||||||
|
modified := *profile
|
||||||
|
|
||||||
|
// Add "(large-db)" suffix to indicate this is modified
|
||||||
|
modified.Name = profile.Name + " +large-db"
|
||||||
|
modified.Description = fmt.Sprintf("%s [LargeDBMode: reduced parallelism, high locks]", profile.Description)
|
||||||
|
|
||||||
|
// Reduce parallelism to avoid lock exhaustion
|
||||||
|
// Rule: halve parallelism, minimum 1
|
||||||
|
modified.ClusterParallelism = max(1, profile.ClusterParallelism/2)
|
||||||
|
modified.Jobs = max(1, profile.Jobs/2)
|
||||||
|
modified.DumpJobs = max(2, profile.DumpJobs/2)
|
||||||
|
|
||||||
|
// Force high max_locks_per_transaction for large schemas
|
||||||
|
modified.MaxLocksPerTxn = 8192
|
||||||
|
|
||||||
|
// Increase maintenance_work_mem for complex operations
|
||||||
|
// Keep or boost maintenance work mem
|
||||||
|
modified.MaintenanceWorkMem = "1GB"
|
||||||
|
if profile.MinMemoryGB >= 32 {
|
||||||
|
modified.MaintenanceWorkMem = "2GB"
|
||||||
|
}
|
||||||
|
|
||||||
|
modified.RecommendedForLarge = true
|
||||||
|
|
||||||
|
return &modified
|
||||||
|
}
|
||||||
|
|
||||||
|
// max returns the larger of two integers
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectMemory detects system memory information
|
||||||
|
func DetectMemory() (*MemoryInfo, error) {
|
||||||
|
info := &MemoryInfo{
|
||||||
|
Platform: runtime.GOOS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
if err := detectLinuxMemory(info); err != nil {
|
||||||
|
return info, fmt.Errorf("linux memory detection failed: %w", err)
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
if err := detectDarwinMemory(info); err != nil {
|
||||||
|
return info, fmt.Errorf("darwin memory detection failed: %w", err)
|
||||||
|
}
|
||||||
|
case "windows":
|
||||||
|
if err := detectWindowsMemory(info); err != nil {
|
||||||
|
return info, fmt.Errorf("windows memory detection failed: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Fallback: use Go runtime memory stats
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
info.TotalBytes = int64(memStats.Sys)
|
||||||
|
info.AvailableBytes = int64(memStats.Sys - memStats.Alloc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate GB values
|
||||||
|
info.TotalGB = int(info.TotalBytes / (1024 * 1024 * 1024))
|
||||||
|
info.AvailableGB = int(info.AvailableBytes / (1024 * 1024 * 1024))
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectLinuxMemory reads memory info from /proc/meminfo
|
||||||
|
func detectLinuxMemory(info *MemoryInfo) error {
|
||||||
|
file, err := os.Open("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSuffix(parts[0], ":")
|
||||||
|
value, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values are in kB
|
||||||
|
valueBytes := value * 1024
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "MemTotal":
|
||||||
|
info.TotalBytes = valueBytes
|
||||||
|
case "MemAvailable":
|
||||||
|
info.AvailableBytes = valueBytes
|
||||||
|
case "MemFree":
|
||||||
|
info.FreeBytes = valueBytes
|
||||||
|
case "SwapTotal":
|
||||||
|
info.SwapTotalBytes = valueBytes
|
||||||
|
case "SwapFree":
|
||||||
|
info.SwapFreeBytes = valueBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.UsedBytes = info.TotalBytes - info.AvailableBytes
|
||||||
|
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectDarwinMemory detects memory on macOS
|
||||||
|
func detectDarwinMemory(info *MemoryInfo) error {
|
||||||
|
// Use sysctl for total memory
|
||||||
|
if output, err := runCommand("sysctl", "-n", "hw.memsize"); err == nil {
|
||||||
|
if val, err := strconv.ParseInt(strings.TrimSpace(output), 10, 64); err == nil {
|
||||||
|
info.TotalBytes = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use vm_stat for available memory (more complex parsing required)
|
||||||
|
if output, err := runCommand("vm_stat"); err == nil {
|
||||||
|
pageSize := int64(4096) // Default page size
|
||||||
|
var freePages, inactivePages int64
|
||||||
|
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "page size of") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
for i, p := range parts {
|
||||||
|
if p == "of" && i+1 < len(parts) {
|
||||||
|
if ps, err := strconv.ParseInt(parts[i+1], 10, 64); err == nil {
|
||||||
|
pageSize = ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.Contains(line, "Pages free:") {
|
||||||
|
val := extractNumberFromLine(line)
|
||||||
|
freePages = val
|
||||||
|
} else if strings.Contains(line, "Pages inactive:") {
|
||||||
|
val := extractNumberFromLine(line)
|
||||||
|
inactivePages = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.FreeBytes = freePages * pageSize
|
||||||
|
info.AvailableBytes = (freePages + inactivePages) * pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
info.UsedBytes = info.TotalBytes - info.AvailableBytes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectWindowsMemory detects memory on Windows
|
||||||
|
func detectWindowsMemory(info *MemoryInfo) error {
|
||||||
|
// Use wmic for memory info
|
||||||
|
if output, err := runCommand("wmic", "OS", "get", "TotalVisibleMemorySize,FreePhysicalMemory", "/format:list"); err == nil {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "TotalVisibleMemorySize=") {
|
||||||
|
val := strings.TrimPrefix(line, "TotalVisibleMemorySize=")
|
||||||
|
if v, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil {
|
||||||
|
info.TotalBytes = v * 1024 // KB to bytes
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, "FreePhysicalMemory=") {
|
||||||
|
val := strings.TrimPrefix(line, "FreePhysicalMemory=")
|
||||||
|
if v, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil {
|
||||||
|
info.FreeBytes = v * 1024
|
||||||
|
info.AvailableBytes = v * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.UsedBytes = info.TotalBytes - info.AvailableBytes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendProfile recommends a resource profile based on system resources and workload
|
||||||
|
func RecommendProfile(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB bool) *ResourceProfile {
|
||||||
|
cores := 0
|
||||||
|
if cpuInfo != nil {
|
||||||
|
cores = cpuInfo.PhysicalCores
|
||||||
|
if cores == 0 {
|
||||||
|
cores = cpuInfo.LogicalCores
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cores == 0 {
|
||||||
|
cores = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
|
||||||
|
memGB := 0
|
||||||
|
if memInfo != nil {
|
||||||
|
memGB = memInfo.TotalGB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: large databases should use conservative profile
|
||||||
|
// The caller should also enable LargeDBMode for increased MaxLocksPerTxn
|
||||||
|
if isLargeDB {
|
||||||
|
// For large DBs, recommend conservative regardless of resources
|
||||||
|
// LargeDBMode flag will handle the lock settings separately
|
||||||
|
return &ProfileConservative
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource-based selection
|
||||||
|
if cores >= 16 && memGB >= 64 {
|
||||||
|
return &ProfileMaxPerformance
|
||||||
|
} else if cores >= 8 && memGB >= 32 {
|
||||||
|
return &ProfilePerformance
|
||||||
|
} else if cores >= 4 && memGB >= 16 {
|
||||||
|
return &ProfileBalanced
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to conservative for constrained systems
|
||||||
|
return &ProfileConservative
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendProfileWithReason returns a profile recommendation with explanation
|
||||||
|
func RecommendProfileWithReason(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB bool) (*ResourceProfile, string) {
|
||||||
|
cores := 0
|
||||||
|
if cpuInfo != nil {
|
||||||
|
cores = cpuInfo.PhysicalCores
|
||||||
|
if cores == 0 {
|
||||||
|
cores = cpuInfo.LogicalCores
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cores == 0 {
|
||||||
|
cores = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
|
||||||
|
memGB := 0
|
||||||
|
if memInfo != nil {
|
||||||
|
memGB = memInfo.TotalGB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build reason string
|
||||||
|
var reason strings.Builder
|
||||||
|
reason.WriteString(fmt.Sprintf("System: %d cores, %dGB RAM. ", cores, memGB))
|
||||||
|
|
||||||
|
profile := RecommendProfile(cpuInfo, memInfo, isLargeDB)
|
||||||
|
|
||||||
|
if isLargeDB {
|
||||||
|
reason.WriteString("Large database mode - using conservative settings. Enable LargeDBMode for higher max_locks.")
|
||||||
|
} else if profile.Name == "conservative" {
|
||||||
|
reason.WriteString("Limited resources detected - using conservative profile for stability.")
|
||||||
|
} else if profile.Name == "max-performance" {
|
||||||
|
reason.WriteString("High-end server detected - using maximum parallelism.")
|
||||||
|
} else if profile.Name == "performance" {
|
||||||
|
reason.WriteString("Good resources detected - using performance profile.")
|
||||||
|
} else {
|
||||||
|
reason.WriteString("Using balanced profile for optimal performance/stability trade-off.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile, reason.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateProfileForSystem checks if a profile is suitable for the current system
|
||||||
|
func ValidateProfileForSystem(profile *ResourceProfile, cpuInfo *CPUInfo, memInfo *MemoryInfo) (bool, []string) {
|
||||||
|
var warnings []string
|
||||||
|
|
||||||
|
cores := 0
|
||||||
|
if cpuInfo != nil {
|
||||||
|
cores = cpuInfo.PhysicalCores
|
||||||
|
if cores == 0 {
|
||||||
|
cores = cpuInfo.LogicalCores
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cores == 0 {
|
||||||
|
cores = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
|
||||||
|
memGB := 0
|
||||||
|
if memInfo != nil {
|
||||||
|
memGB = memInfo.TotalGB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check minimum requirements
|
||||||
|
if cores < profile.MinCores {
|
||||||
|
warnings = append(warnings,
|
||||||
|
fmt.Sprintf("Profile '%s' recommends %d+ cores (system has %d)", profile.Name, profile.MinCores, cores))
|
||||||
|
}
|
||||||
|
|
||||||
|
if memGB < profile.MinMemoryGB {
|
||||||
|
warnings = append(warnings,
|
||||||
|
fmt.Sprintf("Profile '%s' recommends %dGB+ RAM (system has %dGB)", profile.Name, profile.MinMemoryGB, memGB))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for potential issues
|
||||||
|
if profile.ClusterParallelism > cores {
|
||||||
|
warnings = append(warnings,
|
||||||
|
fmt.Sprintf("Cluster parallelism (%d) exceeds CPU cores (%d) - may cause contention",
|
||||||
|
profile.ClusterParallelism, cores))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory pressure warning
|
||||||
|
memPerWorker := 2 // Rough estimate: 2GB per parallel worker for large DB operations
|
||||||
|
requiredMem := profile.ClusterParallelism * profile.Jobs * memPerWorker
|
||||||
|
if memGB > 0 && requiredMem > memGB {
|
||||||
|
warnings = append(warnings,
|
||||||
|
fmt.Sprintf("High parallelism may require ~%dGB RAM (system has %dGB) - risk of OOM",
|
||||||
|
requiredMem, memGB))
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(warnings) == 0, warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatProfileSummary returns a formatted summary of a profile
|
||||||
|
func (p *ResourceProfile) FormatProfileSummary() string {
|
||||||
|
return fmt.Sprintf("[%s] Parallel: %d DBs, %d jobs | Recommended for large DBs: %v",
|
||||||
|
strings.ToUpper(p.Name),
|
||||||
|
p.ClusterParallelism,
|
||||||
|
p.Jobs,
|
||||||
|
p.RecommendedForLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLRecommendations returns PostgreSQL configuration recommendations for this profile
|
||||||
|
func (p *ResourceProfile) PostgreSQLRecommendations() []string {
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("ALTER SYSTEM SET max_locks_per_transaction = %d;", p.MaxLocksPerTxn),
|
||||||
|
fmt.Sprintf("ALTER SYSTEM SET maintenance_work_mem = '%s';", p.MaintenanceWorkMem),
|
||||||
|
"-- Restart PostgreSQL after changes to max_locks_per_transaction",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
func runCommand(name string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNumberFromLine(line string) int64 {
|
||||||
|
// Extract number before the period at end (e.g., "Pages free: 123456.")
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSuffix(p, ".")
|
||||||
|
if val, err := strconv.ParseInt(p, 10, 64); err == nil && val > 0 {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ type Engine struct {
|
|||||||
progress progress.Indicator
|
progress progress.Indicator
|
||||||
detailedReporter *progress.DetailedReporter
|
detailedReporter *progress.DetailedReporter
|
||||||
dryRun bool
|
dryRun bool
|
||||||
|
silentMode bool // Suppress stdout output (for TUI mode)
|
||||||
debugLogPath string // Path to save debug log on error
|
debugLogPath string // Path to save debug log on error
|
||||||
|
|
||||||
// TUI progress callback for detailed progress reporting
|
// TUI progress callback for detailed progress reporting
|
||||||
@@ -86,6 +87,7 @@ func NewSilent(cfg *config.Config, log logger.Logger, db database.Database) *Eng
|
|||||||
progress: progressIndicator,
|
progress: progressIndicator,
|
||||||
detailedReporter: detailedReporter,
|
detailedReporter: detailedReporter,
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
|
silentMode: true, // Suppress stdout for TUI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -542,7 +542,20 @@ func (e *Engine) calculateRecommendedParallel(result *PreflightResult) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// printPreflightSummary prints a nice summary of all checks
|
// printPreflightSummary prints a nice summary of all checks
|
||||||
|
// In silent mode (TUI), this is skipped and results are logged instead
|
||||||
func (e *Engine) printPreflightSummary(result *PreflightResult) {
|
func (e *Engine) printPreflightSummary(result *PreflightResult) {
|
||||||
|
// In TUI/silent mode, don't print to stdout - it causes scrambled output
|
||||||
|
if e.silentMode {
|
||||||
|
// Log summary instead for debugging
|
||||||
|
e.log.Info("Preflight checks complete",
|
||||||
|
"can_proceed", result.CanProceed,
|
||||||
|
"warnings", len(result.Warnings),
|
||||||
|
"errors", len(result.Errors),
|
||||||
|
"total_blobs", result.Archive.TotalBlobCount,
|
||||||
|
"recommended_locks", result.Archive.RecommendedLockBoost)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println(strings.Repeat("─", 60))
|
fmt.Println(strings.Repeat("─", 60))
|
||||||
fmt.Println(" PREFLIGHT CHECKS")
|
fmt.Println(" PREFLIGHT CHECKS")
|
||||||
|
|||||||
@@ -334,10 +334,12 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
|
|||||||
"-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName),
|
"-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add -h flag if host is not localhost (to use Unix socket for peer auth)
|
// Always add -h flag for explicit host connection (required for password auth)
|
||||||
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" {
|
host := s.cfg.Host
|
||||||
args = append([]string{"-h", s.cfg.Host}, args...)
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
}
|
}
|
||||||
|
args = append([]string{"-h", host}, args...)
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "psql", args...)
|
cmd := exec.CommandContext(ctx, "psql", args...)
|
||||||
|
|
||||||
@@ -346,9 +348,9 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
|
|||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to check database existence: %w", err)
|
return false, fmt.Errorf("failed to check database existence: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(string(output)) == "1", nil
|
return strings.TrimSpace(string(output)) == "1", nil
|
||||||
@@ -405,21 +407,29 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
|
|||||||
"-c", query,
|
"-c", query,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add -h flag if host is not localhost (to use Unix socket for peer auth)
|
// Always add -h flag for explicit host connection (required for password auth)
|
||||||
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" {
|
// Empty or unset host defaults to localhost
|
||||||
args = append([]string{"-h", s.cfg.Host}, args...)
|
host := s.cfg.Host
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
}
|
}
|
||||||
|
args = append([]string{"-h", host}, args...)
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "psql", args...)
|
cmd := exec.CommandContext(ctx, "psql", args...)
|
||||||
|
|
||||||
// Set password if provided
|
// Set password - check config first, then environment
|
||||||
|
env := os.Environ()
|
||||||
if s.cfg.Password != "" {
|
if s.cfg.Password != "" {
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
env = append(env, fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
||||||
}
|
}
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
output, err := cmd.Output()
|
s.log.Debug("Listing PostgreSQL databases", "host", host, "port", s.cfg.Port, "user", s.cfg.User)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list databases: %w", err)
|
// Include psql output in error for debugging
|
||||||
|
return nil, fmt.Errorf("failed to list databases: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse output
|
// Parse output
|
||||||
@@ -432,6 +442,8 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.Debug("Found user databases", "count", len(databases), "databases", databases, "raw_output", string(output))
|
||||||
|
|
||||||
return databases, nil
|
return databases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -454,7 +454,6 @@ func (m BackupExecutionModel) View() string {
|
|||||||
} else {
|
} else {
|
||||||
// Show completion summary with detailed stats
|
// Show completion summary with detailed stats
|
||||||
if m.err != nil {
|
if m.err != nil {
|
||||||
s.WriteString("\n")
|
|
||||||
s.WriteString(errorStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
s.WriteString(errorStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(errorStyle.Render("║ [FAIL] BACKUP FAILED ║"))
|
s.WriteString(errorStyle.Render("║ [FAIL] BACKUP FAILED ║"))
|
||||||
|
|||||||
@@ -299,9 +299,13 @@ func (m *MenuModel) View() string {
|
|||||||
|
|
||||||
var s string
|
var s string
|
||||||
|
|
||||||
|
// Product branding header
|
||||||
|
brandLine := fmt.Sprintf("dbbackup v%s • Enterprise Database Backup & Recovery", m.config.Version)
|
||||||
|
s += "\n" + infoStyle.Render(brandLine) + "\n"
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
header := titleStyle.Render("Database Backup Tool - Interactive Menu")
|
header := titleStyle.Render("Interactive Menu")
|
||||||
s += fmt.Sprintf("\n%s\n\n", header)
|
s += fmt.Sprintf("%s\n\n", header)
|
||||||
|
|
||||||
if len(m.dbTypes) > 0 {
|
if len(m.dbTypes) > 0 {
|
||||||
options := make([]string, len(m.dbTypes))
|
options := make([]string, len(m.dbTypes))
|
||||||
|
|||||||
@@ -273,26 +273,42 @@ func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config
|
|||||||
defer dbClient.Close()
|
defer dbClient.Close()
|
||||||
|
|
||||||
// STEP 1: Clean cluster if requested (drop all existing user databases)
|
// STEP 1: Clean cluster if requested (drop all existing user databases)
|
||||||
if restoreType == "restore-cluster" && cleanClusterFirst && len(existingDBs) > 0 {
|
if restoreType == "restore-cluster" && cleanClusterFirst {
|
||||||
log.Info("Dropping existing user databases before cluster restore", "count", len(existingDBs))
|
// Re-detect databases at execution time to get current state
|
||||||
|
// The preview list may be stale or detection may have failed earlier
|
||||||
// Drop databases using command-line psql (no connection required)
|
safety := restore.NewSafety(cfg, log)
|
||||||
// This matches how cluster restore works - uses CLI tools, not database connections
|
currentDBs, err := safety.ListUserDatabases(ctx)
|
||||||
droppedCount := 0
|
if err != nil {
|
||||||
for _, dbName := range existingDBs {
|
log.Warn("Failed to list databases for cleanup, using preview list", "error", err)
|
||||||
// Create timeout context for each database drop (5 minutes per DB - large DBs take time)
|
currentDBs = existingDBs // Fall back to preview list
|
||||||
dropCtx, dropCancel := context.WithTimeout(ctx, 5*time.Minute)
|
} else if len(currentDBs) > 0 {
|
||||||
if err := dropDatabaseCLI(dropCtx, cfg, dbName); err != nil {
|
log.Info("Re-detected user databases for cleanup", "count", len(currentDBs), "databases", currentDBs)
|
||||||
log.Warn("Failed to drop database", "name", dbName, "error", err)
|
existingDBs = currentDBs // Update with fresh list
|
||||||
// Continue with other databases
|
|
||||||
} else {
|
|
||||||
droppedCount++
|
|
||||||
log.Info("Dropped database", "name", dbName)
|
|
||||||
}
|
|
||||||
dropCancel() // Clean up context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Cluster cleanup completed", "dropped", droppedCount, "total", len(existingDBs))
|
if len(existingDBs) > 0 {
|
||||||
|
log.Info("Dropping existing user databases before cluster restore", "count", len(existingDBs))
|
||||||
|
|
||||||
|
// Drop databases using command-line psql (no connection required)
|
||||||
|
// This matches how cluster restore works - uses CLI tools, not database connections
|
||||||
|
droppedCount := 0
|
||||||
|
for _, dbName := range existingDBs {
|
||||||
|
// Create timeout context for each database drop (5 minutes per DB - large DBs take time)
|
||||||
|
dropCtx, dropCancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
|
if err := dropDatabaseCLI(dropCtx, cfg, dbName); err != nil {
|
||||||
|
log.Warn("Failed to drop database", "name", dbName, "error", err)
|
||||||
|
// Continue with other databases
|
||||||
|
} else {
|
||||||
|
droppedCount++
|
||||||
|
log.Info("Dropped database", "name", dbName)
|
||||||
|
}
|
||||||
|
dropCancel() // Clean up context
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Cluster cleanup completed", "dropped", droppedCount, "total", len(existingDBs))
|
||||||
|
} else {
|
||||||
|
log.Info("No user databases to clean up")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 2: Create restore engine with silent progress (no stdout interference with TUI)
|
// STEP 2: Create restore engine with silent progress (no stdout interference with TUI)
|
||||||
@@ -643,7 +659,13 @@ func (m RestoreExecutionModel) View() string {
|
|||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
|
s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err)))
|
|
||||||
|
// Parse and display error in a clean, structured format
|
||||||
|
errStr := m.err.Error()
|
||||||
|
|
||||||
|
// Extract key parts from the error message
|
||||||
|
errDisplay := formatRestoreError(errStr)
|
||||||
|
s.WriteString(errDisplay)
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
||||||
@@ -989,3 +1011,188 @@ func dropDatabaseCLI(ctx context.Context, cfg *config.Config, dbName string) err
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatRestoreError formats a restore error message for clean TUI display
|
||||||
|
func formatRestoreError(errStr string) string {
|
||||||
|
var s strings.Builder
|
||||||
|
maxLineWidth := 60
|
||||||
|
|
||||||
|
// Common patterns to extract
|
||||||
|
patterns := []struct {
|
||||||
|
key string
|
||||||
|
pattern string
|
||||||
|
}{
|
||||||
|
{"Error Type", "ERROR:"},
|
||||||
|
{"Hint", "HINT:"},
|
||||||
|
{"Last Error", "last error:"},
|
||||||
|
{"Total Errors", "total errors:"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try to extract a clean error summary
|
||||||
|
errLines := strings.Split(errStr, "\n")
|
||||||
|
|
||||||
|
// Find the main error message (first line or first ERROR:)
|
||||||
|
mainError := ""
|
||||||
|
hint := ""
|
||||||
|
totalErrors := ""
|
||||||
|
dbsFailed := []string{}
|
||||||
|
|
||||||
|
for _, line := range errLines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ERROR messages
|
||||||
|
if strings.Contains(line, "ERROR:") {
|
||||||
|
if mainError == "" {
|
||||||
|
// Get just the ERROR part
|
||||||
|
idx := strings.Index(line, "ERROR:")
|
||||||
|
if idx >= 0 {
|
||||||
|
mainError = strings.TrimSpace(line[idx:])
|
||||||
|
// Truncate if too long
|
||||||
|
if len(mainError) > maxLineWidth {
|
||||||
|
mainError = mainError[:maxLineWidth-3] + "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract HINT
|
||||||
|
if strings.Contains(line, "HINT:") {
|
||||||
|
idx := strings.Index(line, "HINT:")
|
||||||
|
if idx >= 0 {
|
||||||
|
hint = strings.TrimSpace(line[idx+5:])
|
||||||
|
if len(hint) > maxLineWidth {
|
||||||
|
hint = hint[:maxLineWidth-3] + "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract total errors count
|
||||||
|
if strings.Contains(line, "total errors:") {
|
||||||
|
idx := strings.Index(line, "total errors:")
|
||||||
|
if idx >= 0 {
|
||||||
|
totalErrors = strings.TrimSpace(line[idx+13:])
|
||||||
|
// Just extract the number
|
||||||
|
parts := strings.Fields(totalErrors)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
totalErrors = parts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract failed database names (for cluster restore)
|
||||||
|
if strings.Contains(line, ": restore failed:") {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
dbName := strings.TrimSpace(parts[0])
|
||||||
|
if dbName != "" && !strings.HasPrefix(dbName, "Error") {
|
||||||
|
dbsFailed = append(dbsFailed, dbName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no structured error found, use the first line
|
||||||
|
if mainError == "" {
|
||||||
|
firstLine := errStr
|
||||||
|
if idx := strings.Index(errStr, "\n"); idx > 0 {
|
||||||
|
firstLine = errStr[:idx]
|
||||||
|
}
|
||||||
|
if len(firstLine) > maxLineWidth*2 {
|
||||||
|
firstLine = firstLine[:maxLineWidth*2-3] + "..."
|
||||||
|
}
|
||||||
|
mainError = firstLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build structured error display
|
||||||
|
s.WriteString(infoStyle.Render(" ─── Error Details ─────────────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Error type detection
|
||||||
|
errorType := "critical"
|
||||||
|
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
|
||||||
|
errorType = "critical"
|
||||||
|
} else if strings.Contains(errStr, "connection") {
|
||||||
|
errorType = "connection"
|
||||||
|
} else if strings.Contains(errStr, "permission") || strings.Contains(errStr, "access") {
|
||||||
|
errorType = "permission"
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(fmt.Sprintf(" Type: %s\n", errorType))
|
||||||
|
s.WriteString(fmt.Sprintf(" Message: %s\n", mainError))
|
||||||
|
|
||||||
|
if hint != "" {
|
||||||
|
s.WriteString(fmt.Sprintf(" Hint: %s\n", hint))
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalErrors != "" {
|
||||||
|
s.WriteString(fmt.Sprintf(" Total Errors: %s\n", totalErrors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show failed databases (max 5)
|
||||||
|
if len(dbsFailed) > 0 {
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(" Failed Databases:\n")
|
||||||
|
for i, db := range dbsFailed {
|
||||||
|
if i >= 5 {
|
||||||
|
s.WriteString(fmt.Sprintf(" ... and %d more\n", len(dbsFailed)-5))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" • %s\n", db))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(infoStyle.Render(" ─── Diagnosis ─────────────────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Provide specific recommendations based on error
|
||||||
|
if strings.Contains(errStr, "out of shared memory") || strings.Contains(errStr, "max_locks_per_transaction") {
|
||||||
|
s.WriteString(errorStyle.Render(" • Cannot access file: stat : no such file or directory\n"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(" Lock table exhausted. Total capacity = max_locks_per_transaction\n")
|
||||||
|
s.WriteString(" × (max_connections + max_prepared_transactions).\n\n")
|
||||||
|
s.WriteString(" If you reduced VM size or max_connections, you need higher\n")
|
||||||
|
s.WriteString(" max_locks_per_transaction to compensate.\n\n")
|
||||||
|
s.WriteString(successStyle.Render(" FIX OPTIONS:\n"))
|
||||||
|
s.WriteString(" 1. Enable 'Large DB Mode' in Settings\n")
|
||||||
|
s.WriteString(" (press 'l' to toggle, reduces parallelism, increases locks)\n\n")
|
||||||
|
s.WriteString(" 2. Increase PostgreSQL locks:\n")
|
||||||
|
s.WriteString(" ALTER SYSTEM SET max_locks_per_transaction = 4096;\n")
|
||||||
|
s.WriteString(" Then RESTART PostgreSQL.\n\n")
|
||||||
|
s.WriteString(" 3. Reduce parallel jobs:\n")
|
||||||
|
s.WriteString(" Set Cluster Parallelism = 1 in Settings\n")
|
||||||
|
} else if strings.Contains(errStr, "connection") || strings.Contains(errStr, "refused") {
|
||||||
|
s.WriteString(" • Database connection failed\n\n")
|
||||||
|
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(" 1. Check database is running\n")
|
||||||
|
s.WriteString(" 2. Verify host, port, and credentials in Settings\n")
|
||||||
|
s.WriteString(" 3. Check firewall/network connectivity\n")
|
||||||
|
} else if strings.Contains(errStr, "permission") || strings.Contains(errStr, "denied") {
|
||||||
|
s.WriteString(" • Permission denied\n\n")
|
||||||
|
s.WriteString(infoStyle.Render(" ─── [HINT] Recommendations ────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(" 1. Verify database user has sufficient privileges\n")
|
||||||
|
s.WriteString(" 2. Grant CREATE/DROP DATABASE permissions if restoring cluster\n")
|
||||||
|
s.WriteString(" 3. Check file system permissions on backup directory\n")
|
||||||
|
} else {
|
||||||
|
s.WriteString(" See error message above for details.\n\n")
|
||||||
|
s.WriteString(infoStyle.Render(" ─── [HINT] General Recommendations ────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(" 1. Check the full error log for details\n")
|
||||||
|
s.WriteString(" 2. Try restoring with 'conservative' profile (press 'c')\n")
|
||||||
|
s.WriteString(" 3. For complex databases, enable 'Large DB Mode' (press 'l')\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
|
// Suppress the pattern variable since we don't use it but defined it
|
||||||
|
_ = patterns
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type RestorePreviewModel struct {
|
|||||||
cleanClusterFirst bool // For cluster restore: drop all user databases first
|
cleanClusterFirst bool // For cluster restore: drop all user databases first
|
||||||
existingDBCount int // Number of existing user databases
|
existingDBCount int // Number of existing user databases
|
||||||
existingDBs []string // List of existing user databases
|
existingDBs []string // List of existing user databases
|
||||||
|
existingDBError string // Error message if database listing failed
|
||||||
safetyChecks []SafetyCheck
|
safetyChecks []SafetyCheck
|
||||||
checking bool
|
checking bool
|
||||||
canProceed bool
|
canProceed bool
|
||||||
@@ -102,6 +103,7 @@ type safetyCheckCompleteMsg struct {
|
|||||||
canProceed bool
|
canProceed bool
|
||||||
existingDBCount int
|
existingDBCount int
|
||||||
existingDBs []string
|
existingDBs []string
|
||||||
|
existingDBError string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
||||||
@@ -221,10 +223,12 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
|
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
|
||||||
|
|
||||||
// Get list of existing user databases (exclude templates and system DBs)
|
// Get list of existing user databases (exclude templates and system DBs)
|
||||||
|
var existingDBError string
|
||||||
dbList, err := safety.ListUserDatabases(ctx)
|
dbList, err := safety.ListUserDatabases(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
check.Status = "warning"
|
check.Status = "warning"
|
||||||
check.Message = fmt.Sprintf("Cannot list databases: %v", err)
|
check.Message = fmt.Sprintf("Cannot list databases: %v", err)
|
||||||
|
existingDBError = err.Error()
|
||||||
} else {
|
} else {
|
||||||
existingDBCount = len(dbList)
|
existingDBCount = len(dbList)
|
||||||
existingDBs = dbList
|
existingDBs = dbList
|
||||||
@@ -238,6 +242,14 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
|
|
||||||
|
return safetyCheckCompleteMsg{
|
||||||
|
checks: checks,
|
||||||
|
canProceed: canProceed,
|
||||||
|
existingDBCount: existingDBCount,
|
||||||
|
existingDBs: existingDBs,
|
||||||
|
existingDBError: existingDBError,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return safetyCheckCompleteMsg{
|
return safetyCheckCompleteMsg{
|
||||||
@@ -257,6 +269,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.canProceed = msg.canProceed
|
m.canProceed = msg.canProceed
|
||||||
m.existingDBCount = msg.existingDBCount
|
m.existingDBCount = msg.existingDBCount
|
||||||
m.existingDBs = msg.existingDBs
|
m.existingDBs = msg.existingDBs
|
||||||
|
m.existingDBError = msg.existingDBError
|
||||||
// Auto-forward in auto-confirm mode
|
// Auto-forward in auto-confirm mode
|
||||||
if m.config.TUIAutoConfirm {
|
if m.config.TUIAutoConfirm {
|
||||||
return m.parent, tea.Quit
|
return m.parent, tea.Quit
|
||||||
@@ -275,10 +288,17 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case "c":
|
case "c":
|
||||||
if m.mode == "restore-cluster" {
|
if m.mode == "restore-cluster" {
|
||||||
// Toggle cluster cleanup
|
// Toggle cluster cleanup - databases will be re-detected at execution time
|
||||||
m.cleanClusterFirst = !m.cleanClusterFirst
|
m.cleanClusterFirst = !m.cleanClusterFirst
|
||||||
if m.cleanClusterFirst {
|
if m.cleanClusterFirst {
|
||||||
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
|
if m.existingDBError != "" {
|
||||||
|
// Detection failed in preview - will re-detect at execution
|
||||||
|
m.message = checkWarningStyle.Render("[WARN] Will clean existing databases before restore (detection pending)")
|
||||||
|
} else if m.existingDBCount > 0 {
|
||||||
|
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
|
||||||
|
} else {
|
||||||
|
m.message = infoStyle.Render("[INFO] Cleanup enabled (no databases currently detected)")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.message = fmt.Sprintf("Clean cluster first: disabled")
|
m.message = fmt.Sprintf("Clean cluster first: disabled")
|
||||||
}
|
}
|
||||||
@@ -382,7 +402,27 @@ func (m RestorePreviewModel) View() string {
|
|||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
||||||
|
|
||||||
if m.existingDBCount > 0 {
|
// Show Resource Profile and CPU Workload settings
|
||||||
|
profile := m.config.GetCurrentProfile()
|
||||||
|
if profile != nil {
|
||||||
|
s.WriteString(fmt.Sprintf(" Resource Profile: %s (Parallel:%d, Jobs:%d)\n",
|
||||||
|
profile.Name, profile.ClusterParallelism, profile.Jobs))
|
||||||
|
} else {
|
||||||
|
s.WriteString(fmt.Sprintf(" Resource Profile: %s\n", m.config.ResourceProfile))
|
||||||
|
}
|
||||||
|
// Show Large DB Mode status
|
||||||
|
if m.config.LargeDBMode {
|
||||||
|
s.WriteString(" Large DB Mode: ON (reduced parallelism, high locks)\n")
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" CPU Workload: %s\n", m.config.CPUWorkloadType))
|
||||||
|
s.WriteString(fmt.Sprintf(" Cluster Parallelism: %d databases\n", m.config.ClusterParallelism))
|
||||||
|
|
||||||
|
if m.existingDBError != "" {
|
||||||
|
// Show warning when database listing failed - but still allow cleanup toggle
|
||||||
|
s.WriteString(checkWarningStyle.Render(" Existing Databases: Detection failed\n"))
|
||||||
|
s.WriteString(infoStyle.Render(fmt.Sprintf(" (%s)\n", m.existingDBError)))
|
||||||
|
s.WriteString(infoStyle.Render(" (Will re-detect at restore time)\n"))
|
||||||
|
} else if m.existingDBCount > 0 {
|
||||||
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
|
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
|
||||||
|
|
||||||
// Show first few database names
|
// Show first few database names
|
||||||
@@ -395,17 +435,20 @@ func (m RestorePreviewModel) View() string {
|
|||||||
}
|
}
|
||||||
s.WriteString(fmt.Sprintf(" - %s\n", db))
|
s.WriteString(fmt.Sprintf(" - %s\n", db))
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanIcon := "[N]"
|
|
||||||
cleanStyle := infoStyle
|
|
||||||
if m.cleanClusterFirst {
|
|
||||||
cleanIcon = "[Y]"
|
|
||||||
cleanStyle = checkWarningStyle
|
|
||||||
}
|
|
||||||
s.WriteString(cleanStyle.Render(fmt.Sprintf(" Clean All First: %s %v (press 'c' to toggle)\n", cleanIcon, m.cleanClusterFirst)))
|
|
||||||
} else {
|
} else {
|
||||||
s.WriteString(" Existing Databases: None (clean slate)\n")
|
s.WriteString(" Existing Databases: None (clean slate)\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always show cleanup toggle for cluster restore
|
||||||
|
cleanIcon := "[N]"
|
||||||
|
cleanStyle := infoStyle
|
||||||
|
if m.cleanClusterFirst {
|
||||||
|
cleanIcon := "[Y]"
|
||||||
|
cleanStyle = checkWarningStyle
|
||||||
|
s.WriteString(cleanStyle.Render(fmt.Sprintf(" Clean All First: %s enabled (press 'c' to toggle)\n", cleanIcon)))
|
||||||
|
} else {
|
||||||
|
s.WriteString(cleanStyle.Render(fmt.Sprintf(" Clean All First: %s disabled (press 'c' to toggle)\n", cleanIcon)))
|
||||||
|
}
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,10 +496,18 @@ func (m RestorePreviewModel) View() string {
|
|||||||
s.WriteString(infoStyle.Render(" All existing data in target database will be dropped!"))
|
s.WriteString(infoStyle.Render(" All existing data in target database will be dropped!"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
if m.cleanClusterFirst && m.existingDBCount > 0 {
|
if m.cleanClusterFirst {
|
||||||
s.WriteString(checkWarningStyle.Render("[DANGER] WARNING: Cluster cleanup enabled"))
|
s.WriteString(checkWarningStyle.Render("[DANGER] WARNING: Cluster cleanup enabled"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(checkWarningStyle.Render(fmt.Sprintf(" %d existing database(s) will be DROPPED before restore!", m.existingDBCount)))
|
if m.existingDBError != "" {
|
||||||
|
s.WriteString(checkWarningStyle.Render(" Existing databases will be DROPPED before restore!"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(infoStyle.Render(" (Database count will be detected at restore time)"))
|
||||||
|
} else if m.existingDBCount > 0 {
|
||||||
|
s.WriteString(checkWarningStyle.Render(fmt.Sprintf(" %d existing database(s) will be DROPPED before restore!", m.existingDBCount)))
|
||||||
|
} else {
|
||||||
|
s.WriteString(infoStyle.Render(" No databases currently detected - cleanup will verify at restore time"))
|
||||||
|
}
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(infoStyle.Render(" This ensures a clean disaster recovery scenario"))
|
s.WriteString(infoStyle.Render(" This ensures a clean disaster recovery scenario"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
"dbbackup/internal/config"
|
"dbbackup/internal/config"
|
||||||
|
"dbbackup/internal/cpu"
|
||||||
"dbbackup/internal/logger"
|
"dbbackup/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,6 +102,65 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S
|
|||||||
Type: "selector",
|
Type: "selector",
|
||||||
Description: "CPU workload profile (press Enter to cycle: Balanced → CPU-Intensive → I/O-Intensive)",
|
Description: "CPU workload profile (press Enter to cycle: Balanced → CPU-Intensive → I/O-Intensive)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: "resource_profile",
|
||||||
|
DisplayName: "Resource Profile",
|
||||||
|
Value: func(c *config.Config) string {
|
||||||
|
profile := c.GetCurrentProfile()
|
||||||
|
if profile != nil {
|
||||||
|
return fmt.Sprintf("%s (P:%d J:%d)", profile.Name, profile.ClusterParallelism, profile.Jobs)
|
||||||
|
}
|
||||||
|
return c.ResourceProfile
|
||||||
|
},
|
||||||
|
Update: func(c *config.Config, v string) error {
|
||||||
|
profiles := []string{"conservative", "balanced", "performance", "max-performance"}
|
||||||
|
currentIdx := 0
|
||||||
|
for i, p := range profiles {
|
||||||
|
if c.ResourceProfile == p {
|
||||||
|
currentIdx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextIdx := (currentIdx + 1) % len(profiles)
|
||||||
|
return c.ApplyResourceProfile(profiles[nextIdx])
|
||||||
|
},
|
||||||
|
Type: "selector",
|
||||||
|
Description: "Resource profile for VM capacity. Toggle 'l' for Large DB Mode on any profile.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "large_db_mode",
|
||||||
|
DisplayName: "Large DB Mode",
|
||||||
|
Value: func(c *config.Config) string {
|
||||||
|
if c.LargeDBMode {
|
||||||
|
return "ON (↓parallelism, ↑locks)"
|
||||||
|
}
|
||||||
|
return "OFF"
|
||||||
|
},
|
||||||
|
Update: func(c *config.Config, v string) error {
|
||||||
|
c.LargeDBMode = !c.LargeDBMode
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Type: "selector",
|
||||||
|
Description: "Enable for databases with many tables/LOBs. Reduces parallelism, increases max_locks_per_transaction.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "cluster_parallelism",
|
||||||
|
DisplayName: "Cluster Parallelism",
|
||||||
|
Value: func(c *config.Config) string { return fmt.Sprintf("%d", c.ClusterParallelism) },
|
||||||
|
Update: func(c *config.Config, v string) error {
|
||||||
|
val, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cluster parallelism must be a number")
|
||||||
|
}
|
||||||
|
if val < 1 {
|
||||||
|
return fmt.Errorf("cluster parallelism must be at least 1")
|
||||||
|
}
|
||||||
|
c.ClusterParallelism = val
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Type: "int",
|
||||||
|
Description: "Concurrent databases during cluster backup/restore (1=sequential, safer for large DBs)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Key: "backup_dir",
|
Key: "backup_dir",
|
||||||
DisplayName: "Backup Directory",
|
DisplayName: "Backup Directory",
|
||||||
@@ -528,12 +588,70 @@ func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case "s":
|
case "s":
|
||||||
return m.saveSettings()
|
return m.saveSettings()
|
||||||
|
|
||||||
|
case "l":
|
||||||
|
// Quick shortcut: Toggle Large DB Mode
|
||||||
|
return m.toggleLargeDBMode()
|
||||||
|
|
||||||
|
case "c":
|
||||||
|
// Quick shortcut: Apply "conservative" profile for constrained VMs
|
||||||
|
return m.applyConservativeProfile()
|
||||||
|
|
||||||
|
case "p":
|
||||||
|
// Show profile recommendation
|
||||||
|
return m.showProfileRecommendation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toggleLargeDBMode toggles the Large DB Mode flag
|
||||||
|
func (m SettingsModel) toggleLargeDBMode() (tea.Model, tea.Cmd) {
|
||||||
|
m.config.LargeDBMode = !m.config.LargeDBMode
|
||||||
|
if m.config.LargeDBMode {
|
||||||
|
profile := m.config.GetCurrentProfile()
|
||||||
|
m.message = successStyle.Render(fmt.Sprintf(
|
||||||
|
"[ON] Large DB Mode enabled: %s → Parallel=%d, Jobs=%d, MaxLocks=%d",
|
||||||
|
profile.Name, profile.ClusterParallelism, profile.Jobs, profile.MaxLocksPerTxn))
|
||||||
|
} else {
|
||||||
|
profile := m.config.GetCurrentProfile()
|
||||||
|
m.message = successStyle.Render(fmt.Sprintf(
|
||||||
|
"[OFF] Large DB Mode disabled: %s → Parallel=%d, Jobs=%d",
|
||||||
|
profile.Name, profile.ClusterParallelism, profile.Jobs))
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyConservativeProfile applies the conservative profile for constrained VMs
|
||||||
|
func (m SettingsModel) applyConservativeProfile() (tea.Model, tea.Cmd) {
|
||||||
|
if err := m.config.ApplyResourceProfile("conservative"); err != nil {
|
||||||
|
m.message = errorStyle.Render(fmt.Sprintf("[FAIL] %s", err.Error()))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.message = successStyle.Render("[OK] Applied 'conservative' profile: Cluster=1, Jobs=1. Safe for small VMs with limited memory.")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// showProfileRecommendation displays the recommended profile based on system resources
|
||||||
|
func (m SettingsModel) showProfileRecommendation() (tea.Model, tea.Cmd) {
|
||||||
|
profileName, reason := m.config.GetResourceProfileRecommendation(false)
|
||||||
|
|
||||||
|
var largeDBHint string
|
||||||
|
if m.config.LargeDBMode {
|
||||||
|
largeDBHint = "Large DB Mode: ON"
|
||||||
|
} else {
|
||||||
|
largeDBHint = "Large DB Mode: OFF (press 'l' to enable)"
|
||||||
|
}
|
||||||
|
|
||||||
|
m.message = infoStyle.Render(fmt.Sprintf(
|
||||||
|
"[RECOMMEND] Profile: %s | %s\n"+
|
||||||
|
" → %s\n"+
|
||||||
|
" Press 'l' to toggle Large DB Mode, 'c' for conservative",
|
||||||
|
profileName, largeDBHint, reason))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleEditingInput handles input when editing a setting
|
// handleEditingInput handles input when editing a setting
|
||||||
func (m SettingsModel) handleEditingInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m SettingsModel) handleEditingInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
@@ -747,7 +865,32 @@ func (m SettingsModel) View() string {
|
|||||||
// Current configuration summary
|
// Current configuration summary
|
||||||
if !m.editing {
|
if !m.editing {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(infoStyle.Render("[INFO] Current Configuration"))
|
b.WriteString(infoStyle.Render("[INFO] System Resources & Configuration"))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
// System resources
|
||||||
|
var sysInfo []string
|
||||||
|
if m.config.CPUInfo != nil {
|
||||||
|
sysInfo = append(sysInfo, fmt.Sprintf("CPU: %d cores (physical), %d logical",
|
||||||
|
m.config.CPUInfo.PhysicalCores, m.config.CPUInfo.LogicalCores))
|
||||||
|
}
|
||||||
|
if m.config.MemoryInfo != nil {
|
||||||
|
sysInfo = append(sysInfo, fmt.Sprintf("Memory: %dGB total, %dGB available",
|
||||||
|
m.config.MemoryInfo.TotalGB, m.config.MemoryInfo.AvailableGB))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommended profile
|
||||||
|
recommendedProfile, reason := m.config.GetResourceProfileRecommendation(false)
|
||||||
|
sysInfo = append(sysInfo, fmt.Sprintf("Recommended Profile: %s", recommendedProfile))
|
||||||
|
sysInfo = append(sysInfo, fmt.Sprintf(" → %s", reason))
|
||||||
|
|
||||||
|
for _, line := range sysInfo {
|
||||||
|
b.WriteString(detailStyle.Render(fmt.Sprintf(" %s", line)))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(infoStyle.Render("[CONFIG] Current Settings"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
summary := []string{
|
summary := []string{
|
||||||
@@ -755,7 +898,17 @@ func (m SettingsModel) View() string {
|
|||||||
fmt.Sprintf("Database: %s@%s:%d", m.config.User, m.config.Host, m.config.Port),
|
fmt.Sprintf("Database: %s@%s:%d", m.config.User, m.config.Host, m.config.Port),
|
||||||
fmt.Sprintf("Backup Dir: %s", m.config.BackupDir),
|
fmt.Sprintf("Backup Dir: %s", m.config.BackupDir),
|
||||||
fmt.Sprintf("Compression: Level %d", m.config.CompressionLevel),
|
fmt.Sprintf("Compression: Level %d", m.config.CompressionLevel),
|
||||||
fmt.Sprintf("Jobs: %d parallel, %d dump", m.config.Jobs, m.config.DumpJobs),
|
fmt.Sprintf("Profile: %s | Cluster: %d parallel | Jobs: %d",
|
||||||
|
m.config.ResourceProfile, m.config.ClusterParallelism, m.config.Jobs),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show profile warnings if applicable
|
||||||
|
profile := m.config.GetCurrentProfile()
|
||||||
|
if profile != nil {
|
||||||
|
isValid, warnings := cpu.ValidateProfileForSystem(profile, m.config.CPUInfo, m.config.MemoryInfo)
|
||||||
|
if !isValid && len(warnings) > 0 {
|
||||||
|
summary = append(summary, fmt.Sprintf("⚠️ Warning: %s", warnings[0]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.config.CloudEnabled {
|
if m.config.CloudEnabled {
|
||||||
@@ -782,9 +935,9 @@ func (m SettingsModel) View() string {
|
|||||||
} else {
|
} else {
|
||||||
// Show different help based on current selection
|
// Show different help based on current selection
|
||||||
if m.cursor >= 0 && m.cursor < len(m.settings) && m.settings[m.cursor].Type == "path" {
|
if m.cursor >= 0 && m.cursor < len(m.settings) && m.settings[m.cursor].Type == "path" {
|
||||||
footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | Tab browse directories | 's' save | 'r' reset | 'q' menu")
|
footer = infoStyle.Render("\n[KEYS] ↑↓ navigate | Enter edit | Tab dirs | 'l' toggle LargeDB | 'c' conservative | 'p' recommend | 's' save | 'q' menu")
|
||||||
} else {
|
} else {
|
||||||
footer = infoStyle.Render("\n[KEYS] Up/Down navigate | Enter edit | 's' save | 'r' reset | 'q' menu | Tab=dirs on path fields only")
|
footer = infoStyle.Render("\n[KEYS] ↑↓ navigate | Enter edit | 'l' toggle LargeDB mode | 'c' conservative | 'p' recommend | 's' save | 'r' reset | 'q' menu")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user