Compare commits

...

1 Commits

Author SHA1 Message Date
59a717abe7 refactor(profiles): replace large-db profile with composable LargeDBMode
All checks were successful
CI/CD / Test (push) Successful in 1m17s
CI/CD / Lint (push) Successful in 1m26s
CI/CD / Build & Release (push) Successful in 3m15s
BREAKING CHANGE: Removed 'large-db' as standalone profile

New Design:
- Resource Profiles now purely represent VM capacity:
  conservative, balanced, performance, max-performance
- LargeDBMode is a separate boolean toggle that modifies any profile:
  - Reduces ClusterParallelism and Jobs by 50%
  - Forces MaxLocksPerTxn = 8192
  - Increases MaintenanceWorkMem

TUI Changes:
- 'l' key now toggles LargeDBMode ON/OFF instead of applying large-db profile
- New 'Large DB Mode' setting in settings menu
- Settings are persisted to .dbbackup.conf

This allows any resource profile to be combined with large database
optimization, giving users more flexibility on both small and large VMs.
2026-01-18 12:39:21 +01:00
8 changed files with 169 additions and 70 deletions

View File

@@ -221,7 +221,7 @@ Configuration Settings
Database User: postgres Database User: postgres
SSL Mode: prefer SSL Mode: prefer
[KEYS] ↑↓ navigate | Enter edit | 'l' large-db | 'c' conservative | 'p' recommend | 's' save | 'q' menu [KEYS] ↑↓ navigate | Enter edit | 'l' toggle LargeDB | 'c' conservative | 'p' recommend | 's' save | 'q' menu
``` ```
**Resource Profiles for Large Databases:** **Resource Profiles for Large Databases:**
@@ -233,9 +233,11 @@ When restoring large databases on VMs with limited resources, use the resource p
| conservative | 1 | 1 | Small VMs (<16GB RAM) | | conservative | 1 | 1 | Small VMs (<16GB RAM) |
| balanced | 2 | 2-4 | Medium VMs (16-32GB RAM) | | balanced | 2 | 2-4 | Medium VMs (16-32GB RAM) |
| performance | 4 | 4-8 | Large servers (32GB+ RAM) | | performance | 4 | 4-8 | Large servers (32GB+ RAM) |
| large-db | 1 | 2 | Large databases on any hardware | | max-performance | 8 | 8-16 | High-end servers (64GB+) |
**Quick shortcuts:** Press `l` to apply large-db profile, `c` for conservative, `p` to show recommendation. **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:**
``` ```

View File

@@ -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-18_11:06:11_UTC - **Build Time**: 2026-01-18_11:19:47_UTC
- **Git Commit**: ea4337e - **Git Commit**: 490a12f
## 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

View File

@@ -37,7 +37,8 @@ type Config struct {
CPUWorkloadType string // "cpu-intensive", "io-intensive", "balanced" CPUWorkloadType string // "cpu-intensive", "io-intensive", "balanced"
// Resource profile for backup/restore operations // Resource profile for backup/restore operations
ResourceProfile string // "conservative", "balanced", "performance", "max-performance", "large-db" 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
@@ -209,6 +210,7 @@ func New() *Config {
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, ResourceProfile: defaultProfile,
LargeDBMode: getEnvBool("LARGE_DB_MODE", false),
// CPU and memory detection // CPU and memory detection
CPUDetector: cpuDetector, CPUDetector: cpuDetector,
@@ -430,7 +432,7 @@ func (c *Config) ApplyResourceProfile(profileName string) error {
return &ConfigError{ return &ConfigError{
Field: "resource_profile", Field: "resource_profile",
Value: profileName, Value: profileName,
Message: "unknown profile. Valid profiles: conservative, balanced, performance, max-performance, large-db", Message: "unknown profile. Valid profiles: conservative, balanced, performance, max-performance",
} }
} }
@@ -457,8 +459,19 @@ func (c *Config) GetResourceProfileRecommendation(isLargeDB bool) (string, strin
} }
// GetCurrentProfile returns the current resource profile details // GetCurrentProfile returns the current resource profile details
// If LargeDBMode is enabled, returns a modified profile with reduced parallelism
func (c *Config) GetCurrentProfile() *cpu.ResourceProfile { func (c *Config) GetCurrentProfile() *cpu.ResourceProfile {
return cpu.GetProfileByName(c.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

View File

@@ -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,
} }
} }

View File

@@ -90,32 +90,17 @@ var (
DumpJobs: 16, DumpJobs: 16,
MaintenanceWorkMem: "2GB", MaintenanceWorkMem: "2GB",
MaxLocksPerTxn: 512, MaxLocksPerTxn: 512,
RecommendedForLarge: false, // Large DBs should use conservative RecommendedForLarge: false, // Large DBs should use LargeDBMode
MinMemoryGB: 64, MinMemoryGB: 64,
MinCores: 16, MinCores: 16,
} }
// ProfileLargeDB - Optimized specifically for large databases // AllProfiles contains all available profiles (VM resource-based)
ProfileLargeDB = ResourceProfile{
Name: "large-db",
Description: "Optimized for large databases with many tables/BLOBs. Prevents 'out of shared memory' errors.",
ClusterParallelism: 1,
Jobs: 2,
DumpJobs: 2,
MaintenanceWorkMem: "1GB",
MaxLocksPerTxn: 8192,
RecommendedForLarge: true,
MinMemoryGB: 8,
MinCores: 2,
}
// AllProfiles contains all available profiles
AllProfiles = []ResourceProfile{ AllProfiles = []ResourceProfile{
ProfileConservative, ProfileConservative,
ProfileBalanced, ProfileBalanced,
ProfilePerformance, ProfilePerformance,
ProfileMaxPerformance, ProfileMaxPerformance,
ProfileLargeDB,
} }
) )
@@ -129,6 +114,51 @@ func GetProfileByName(name string) *ResourceProfile {
return nil 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 // DetectMemory detects system memory information
func DetectMemory() (*MemoryInfo, error) { func DetectMemory() (*MemoryInfo, error) {
info := &MemoryInfo{ info := &MemoryInfo{
@@ -293,11 +323,11 @@ func RecommendProfile(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB bool) *Re
memGB = memInfo.TotalGB memGB = memInfo.TotalGB
} }
// Special case: large databases should always use conservative/large-db profile // Special case: large databases should use conservative profile
// The caller should also enable LargeDBMode for increased MaxLocksPerTxn
if isLargeDB { if isLargeDB {
if memGB >= 32 && cores >= 8 { // For large DBs, recommend conservative regardless of resources
return &ProfileLargeDB // Still conservative but with more memory for maintenance // LargeDBMode flag will handle the lock settings separately
}
return &ProfileConservative return &ProfileConservative
} }
@@ -339,7 +369,7 @@ func RecommendProfileWithReason(cpuInfo *CPUInfo, memInfo *MemoryInfo, isLargeDB
profile := RecommendProfile(cpuInfo, memInfo, isLargeDB) profile := RecommendProfile(cpuInfo, memInfo, isLargeDB)
if isLargeDB { if isLargeDB {
reason.WriteString("Large database detected - using conservative settings to avoid 'out of shared memory' errors.") reason.WriteString("Large database mode - using conservative settings. Enable LargeDBMode for higher max_locks.")
} else if profile.Name == "conservative" { } else if profile.Name == "conservative" {
reason.WriteString("Limited resources detected - using conservative profile for stability.") reason.WriteString("Limited resources detected - using conservative profile for stability.")
} else if profile.Name == "max-performance" { } else if profile.Name == "max-performance" {

View File

@@ -1159,8 +1159,8 @@ func formatRestoreError(errStr string) string {
s.WriteString(" If you reduced VM size or max_connections, you need higher\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(" max_locks_per_transaction to compensate.\n\n")
s.WriteString(successStyle.Render(" FIX OPTIONS:\n")) s.WriteString(successStyle.Render(" FIX OPTIONS:\n"))
s.WriteString(" 1. Use 'conservative' or 'large-db' profile in Settings\n") s.WriteString(" 1. Enable 'Large DB Mode' in Settings\n")
s.WriteString(" (press 'l' for large-db, 'c' for conservative)\n\n") s.WriteString(" (press 'l' to toggle, reduces parallelism, increases locks)\n\n")
s.WriteString(" 2. Increase PostgreSQL locks:\n") s.WriteString(" 2. Increase PostgreSQL locks:\n")
s.WriteString(" ALTER SYSTEM SET max_locks_per_transaction = 4096;\n") s.WriteString(" ALTER SYSTEM SET max_locks_per_transaction = 4096;\n")
s.WriteString(" Then RESTART PostgreSQL.\n\n") s.WriteString(" Then RESTART PostgreSQL.\n\n")
@@ -1186,7 +1186,7 @@ func formatRestoreError(errStr string) string {
s.WriteString("\n\n") s.WriteString("\n\n")
s.WriteString(" 1. Check the full error log for details\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(" 2. Try restoring with 'conservative' profile (press 'c')\n")
s.WriteString(" 3. For large databases, use 'large-db' profile (press 'l')\n") s.WriteString(" 3. For complex databases, enable 'Large DB Mode' (press 'l')\n")
} }
s.WriteString("\n") s.WriteString("\n")

View File

@@ -410,6 +410,10 @@ func (m RestorePreviewModel) View() string {
} else { } else {
s.WriteString(fmt.Sprintf(" Resource Profile: %s\n", m.config.ResourceProfile)) 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(" CPU Workload: %s\n", m.config.CPUWorkloadType))
s.WriteString(fmt.Sprintf(" Cluster Parallelism: %d databases\n", m.config.ClusterParallelism)) s.WriteString(fmt.Sprintf(" Cluster Parallelism: %d databases\n", m.config.ClusterParallelism))

View File

@@ -113,7 +113,7 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S
return c.ResourceProfile return c.ResourceProfile
}, },
Update: func(c *config.Config, v string) error { Update: func(c *config.Config, v string) error {
profiles := []string{"conservative", "balanced", "performance", "max-performance", "large-db"} profiles := []string{"conservative", "balanced", "performance", "max-performance"}
currentIdx := 0 currentIdx := 0
for i, p := range profiles { for i, p := range profiles {
if c.ResourceProfile == p { if c.ResourceProfile == p {
@@ -125,7 +125,23 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S
return c.ApplyResourceProfile(profiles[nextIdx]) return c.ApplyResourceProfile(profiles[nextIdx])
}, },
Type: "selector", Type: "selector",
Description: "Resource profile for backup/restore. Use 'conservative' or 'large-db' for large databases on small VMs.", 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", Key: "cluster_parallelism",
@@ -574,8 +590,8 @@ func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.saveSettings() return m.saveSettings()
case "l": case "l":
// Quick shortcut: Apply "large-db" profile for large databases // Quick shortcut: Toggle Large DB Mode
return m.applyLargeDBProfile() return m.toggleLargeDBMode()
case "c": case "c":
// Quick shortcut: Apply "conservative" profile for constrained VMs // Quick shortcut: Apply "conservative" profile for constrained VMs
@@ -590,13 +606,20 @@ func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
// applyLargeDBProfile applies the large-db profile optimized for large databases // toggleLargeDBMode toggles the Large DB Mode flag
func (m SettingsModel) applyLargeDBProfile() (tea.Model, tea.Cmd) { func (m SettingsModel) toggleLargeDBMode() (tea.Model, tea.Cmd) {
if err := m.config.ApplyResourceProfile("large-db"); err != nil { m.config.LargeDBMode = !m.config.LargeDBMode
m.message = errorStyle.Render(fmt.Sprintf("[FAIL] %s", err.Error())) if m.config.LargeDBMode {
return m, nil 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))
} }
m.message = successStyle.Render("[OK] Applied 'large-db' profile: Cluster=1, Jobs=2. Optimized for large DBs to avoid 'out of shared memory' errors.")
return m, nil return m, nil
} }
@@ -613,14 +636,19 @@ func (m SettingsModel) applyConservativeProfile() (tea.Model, tea.Cmd) {
// showProfileRecommendation displays the recommended profile based on system resources // showProfileRecommendation displays the recommended profile based on system resources
func (m SettingsModel) showProfileRecommendation() (tea.Model, tea.Cmd) { func (m SettingsModel) showProfileRecommendation() (tea.Model, tea.Cmd) {
profileName, reason := m.config.GetResourceProfileRecommendation(false) profileName, reason := m.config.GetResourceProfileRecommendation(false)
largeDBProfile, largeDBReason := m.config.GetResourceProfileRecommendation(true)
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( m.message = infoStyle.Render(fmt.Sprintf(
"[RECOMMEND] Default: %s | For Large DBs: %s\n"+ "[RECOMMEND] Profile: %s | %s\n"+
" → %s\n"+ " → %s\n"+
" → Large DB: %s\n"+ " Press 'l' to toggle Large DB Mode, 'c' for conservative",
" Press 'l' for large-db profile, 'c' for conservative", profileName, largeDBHint, reason))
profileName, largeDBProfile, reason, largeDBReason))
return m, nil return m, nil
} }
@@ -907,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] ↑↓ navigate | Enter edit | Tab dirs | 'l' large-db | 'c' conservative | 'p' recommend | 's' save | '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] ↑↓ navigate | Enter edit | 'l' large-db profile | 'c' conservative | 'p' recommend | 's' save | 'r' reset | 'q' menu") footer = infoStyle.Render("\n[KEYS] ↑↓ navigate | Enter edit | 'l' toggle LargeDB mode | 'c' conservative | 'p' recommend | 's' save | 'r' reset | 'q' menu")
} }
} }
} }