feat(database): add a flag to compact on startup BE-12283 (#1256)

release/2.33.2
andres-portainer 2025-09-24 18:43:54 -03:00 committed by GitHub
parent c21c91632f
commit dcfe2d9809
8 changed files with 118 additions and 10 deletions

View File

@ -56,6 +56,7 @@ func CLIFlags() *portainer.CLIFlags {
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(), PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(), TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(), CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
CompactDB: kingpin.Flag("compact-db", "Enable database compaction on startup").Envar(portainer.CompactDBEnvVar).Default("false").Bool(),
} }
} }

View File

@ -84,7 +84,7 @@ func initFileService(dataStorePath string) portainer.FileService {
} }
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore { func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey) connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey, *flags.CompactDB)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed creating database connection") log.Fatal().Err(err).Msg("failed creating database connection")
} }

View File

@ -21,6 +21,9 @@ import (
const ( const (
DatabaseFileName = "portainer.db" DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb" EncryptedDatabaseFileName = "portainer.edb"
txMaxSize = 65536
compactedSuffix = ".compacted"
) )
var ( var (
@ -35,6 +38,7 @@ type DbConnection struct {
InitialMmapSize int InitialMmapSize int
EncryptionKey []byte EncryptionKey []byte
isEncrypted bool isEncrypted bool
Compact bool
*bolt.DB *bolt.DB
} }
@ -135,12 +139,7 @@ func (connection *DbConnection) Open() error {
// Now we open the db // Now we open the db
databasePath := connection.GetDatabaseFilePath() databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{ db, err := bolt.Open(databasePath, 0600, connection.boltOptions())
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
FreelistType: bolt.FreelistMapType,
NoFreelistSync: true,
})
if err != nil { if err != nil {
return err return err
} }
@ -149,6 +148,15 @@ func (connection *DbConnection) Open() error {
db.MaxBatchDelay = connection.MaxBatchDelay db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db connection.DB = db
if connection.Compact {
log.Info().Msg("compacting database")
if err := connection.compact(); err != nil {
log.Error().Err(err).Msg("failed to compact database")
} else {
log.Info().Msg("database compaction completed")
}
}
return nil return nil
} }
@ -414,3 +422,42 @@ func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
return err return err
} }
// compact attempts to compact the database and replace it iff it succeeds
func (connection *DbConnection) compact() error {
compactedPath := connection.GetDatabaseFilePath() + compactedSuffix
compactedDB, err := bolt.Open(compactedPath, 0o600, connection.boltOptions())
if err != nil {
return fmt.Errorf("failure to create the compacted database: %w", err)
}
compactedDB.MaxBatchSize = connection.MaxBatchSize
compactedDB.MaxBatchDelay = connection.MaxBatchDelay
if err := bolt.Compact(compactedDB, connection.DB, txMaxSize); err != nil {
return fmt.Errorf("failure to compact the database: %w",
errors.Join(err, compactedDB.Close(), os.Remove(compactedPath)))
}
if err := os.Rename(compactedPath, connection.GetDatabaseFilePath()); err != nil {
return fmt.Errorf("failure to move the compacted database: %w",
errors.Join(err, compactedDB.Close(), os.Remove(compactedPath)))
}
if err := connection.Close(); err != nil {
log.Warn().Err(err).Msg("failure to close the database after compaction")
}
connection.DB = compactedDB
return nil
}
func (connection *DbConnection) boltOptions() *bolt.Options {
return &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
FreelistType: bolt.FreelistMapType,
NoFreelistSync: true,
}
}

View File

@ -6,6 +6,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
) )
func Test_NeedsEncryptionMigration(t *testing.T) { func Test_NeedsEncryptionMigration(t *testing.T) {
@ -119,3 +121,57 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
}) })
} }
} }
func TestDBCompaction(t *testing.T) {
db := &DbConnection{
Path: t.TempDir(),
Compact: true,
}
err := db.Open()
require.NoError(t, err)
err = db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("testbucket"))
if err != nil {
return err
}
b.Put([]byte("key"), []byte("value"))
return nil
})
require.NoError(t, err)
err = db.Close()
require.NoError(t, err)
// Reopen the DB to trigger compaction
err = db.Open()
require.NoError(t, err)
// Check that the data is still there
err = db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte("testbucket"))
if b == nil {
return nil
}
val := b.Get([]byte("key"))
require.Equal(t, []byte("value"), val)
return nil
})
require.NoError(t, err)
err = db.Close()
require.NoError(t, err)
// Failures
err = os.Mkdir(db.GetDatabaseFilePath()+compactedSuffix, 0o755)
require.NoError(t, err)
err = db.Open()
require.NoError(t, err)
}

View File

@ -8,11 +8,12 @@ import (
) )
// NewDatabase should use config options to return a connection to the requested database // NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) { func NewDatabase(storeType, storePath string, encryptionKey []byte, compact bool) (connection portainer.Connection, err error) {
if storeType == "boltdb" { if storeType == "boltdb" {
return &boltdb.DbConnection{ return &boltdb.DbConnection{
Path: storePath, Path: storePath,
EncryptionKey: encryptionKey, EncryptionKey: encryptionKey,
Compact: compact,
}, nil }, nil
} }

View File

@ -44,7 +44,7 @@ func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error)
secretKey = nil secretKey = nil
} }
connection, err := database.NewDatabase("boltdb", storePath, secretKey) connection, err := database.NewDatabase("boltdb", storePath, secretKey, false)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -113,7 +113,7 @@ type datastoreOption = func(d *testDatastore)
// NewDatastore creates new instance of testDatastore. // NewDatastore creates new instance of testDatastore.
// Will apply options before returning, opts will be applied from left to right. // Will apply options before returning, opts will be applied from left to right.
func NewDatastore(options ...datastoreOption) *testDatastore { func NewDatastore(options ...datastoreOption) *testDatastore {
conn, _ := database.NewDatabase("boltdb", "", nil) conn, _ := database.NewDatabase("boltdb", "", nil, false)
d := testDatastore{connection: conn} d := testDatastore{connection: conn}
for _, o := range options { for _, o := range options {

View File

@ -112,6 +112,7 @@ type (
AdminPasswordFile *string AdminPasswordFile *string
Assets *string Assets *string
CSP *bool CSP *bool
CompactDB *bool
Data *string Data *string
FeatureFlags *[]string FeatureFlags *[]string
EnableEdgeComputeFeatures *bool EnableEdgeComputeFeatures *bool
@ -1846,6 +1847,8 @@ const (
TrustedOriginsEnvVar = "TRUSTED_ORIGINS" TrustedOriginsEnvVar = "TRUSTED_ORIGINS"
// CSPEnvVar is the environment variable used to enable/disable the Content Security Policy // CSPEnvVar is the environment variable used to enable/disable the Content Security Policy
CSPEnvVar = "CSP" CSPEnvVar = "CSP"
// CompactDBEnvVar is the environment variable used to enable/disable the startup compaction of the database
CompactDBEnvVar = "COMPACT_DB"
) )
// List of supported features // List of supported features