diff --git a/engine/internal/cloning/base.go b/engine/internal/cloning/base.go index fa3df44d968ca198fc085c0eec87b63e9677ba11..62f78b16a430efa9e043ee8901a4e4239c4d2590 100644 --- a/engine/internal/cloning/base.go +++ b/engine/internal/cloning/base.go @@ -78,6 +78,8 @@ func (c *Base) Run(ctx context.Context) error { return errors.Wrap(err, "failed to run cloning service") } + c.provision.DiscoverSnapshots() + if _, err := c.GetSnapshots(); err != nil { log.Err("No available snapshots: ", err) } diff --git a/engine/internal/provision/mode_local.go b/engine/internal/provision/mode_local.go index fee7d02beff10b7f9b2911a4c076c96507684c33..3ce3a669315ace7dbf91f72ba8163b28dad97314 100644 --- a/engine/internal/provision/mode_local.go +++ b/engine/internal/provision/mode_local.go @@ -27,7 +27,6 @@ import ( "gitlab.com/postgres-ai/database-lab/v3/internal/provision/pool" "gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources" "gitlab.com/postgres-ai/database-lab/v3/internal/provision/runners" - "gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones/zfs" "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools" "gitlab.com/postgres-ai/database-lab/v3/pkg/log" "gitlab.com/postgres-ai/database-lab/v3/pkg/models" @@ -299,20 +298,22 @@ func (p *Provisioner) ResetSession(session *resources.Session, snapshotID string return snapshotModel, nil } +// DiscoverSnapshots discovers snapshots from active pools. +func (p *Provisioner) DiscoverSnapshots() { + for _, fsManager := range p.pm.GetAvailableFSManagers() { + fsManager.RefreshSnapshotList() + } +} + // GetSnapshots provides a snapshot list from active pools. func (p *Provisioner) GetSnapshots() ([]resources.Snapshot, error) { snapshots := []resources.Snapshot{} - for _, activeFSManager := range p.pm.GetActiveFSManagers() { - poolSnapshots, err := activeFSManager.GetSnapshots() - if err != nil { - var emptyErr *zfs.EmptyPoolError - if errors.As(err, &emptyErr) { - log.Msg(emptyErr.Error()) - continue - } - - log.Err(fmt.Errorf("failed to get snapshots for pool %s: %w", activeFSManager.Pool().Name, err)) + for _, activeFSManager := range p.pm.GetAvailableFSManagers() { + poolSnapshots := activeFSManager.SnapshotList() + if len(poolSnapshots) == 0 { + log.Msg(fmt.Sprintf("no snapshots for pool %s", activeFSManager.Pool().Name)) + continue } snapshots = append(snapshots, poolSnapshots...) diff --git a/engine/internal/provision/mode_local_test.go b/engine/internal/provision/mode_local_test.go index cc9fb8f4daf5c28790b73de74d3b5fd49af1f1b0..7cdeb756df78c390ecb8d7d558a40212ec423b82 100644 --- a/engine/internal/provision/mode_local_test.go +++ b/engine/internal/provision/mode_local_test.go @@ -84,8 +84,11 @@ func (m mockFSManager) CleanupSnapshots(retentionLimit int) ([]string, error) { return nil, nil } -func (m mockFSManager) GetSnapshots() ([]resources.Snapshot, error) { - return nil, nil +func (m mockFSManager) SnapshotList() []resources.Snapshot { + return nil +} + +func (m mockFSManager) RefreshSnapshotList() { } func (m mockFSManager) GetSessionState(name string) (*resources.SessionState, error) { diff --git a/engine/internal/provision/pool/manager.go b/engine/internal/provision/pool/manager.go index 11a5fa4e51ae35f1a90f26716e6e82e1c4c6c9a8..dd517351358232a9bd6a958a55f90acebaa0ffc6 100644 --- a/engine/internal/provision/pool/manager.go +++ b/engine/internal/provision/pool/manager.go @@ -45,7 +45,8 @@ type Snapshotter interface { CreateSnapshot(poolSuffix, dataStateAt string) (snapshotName string, err error) DestroySnapshot(snapshotName string) (err error) CleanupSnapshots(retentionLimit int) ([]string, error) - GetSnapshots() ([]resources.Snapshot, error) + SnapshotList() []resources.Snapshot + RefreshSnapshotList() } // Pooler describes methods for Pool providing. diff --git a/engine/internal/provision/pool/pool_manager.go b/engine/internal/provision/pool/pool_manager.go index a61951403002d6a489e76234dc296765e0868652..e5c2c0f916233b0ee9f56ccf3aa49b9f3f7a4c15 100644 --- a/engine/internal/provision/pool/pool_manager.go +++ b/engine/internal/provision/pool/pool_manager.go @@ -193,14 +193,14 @@ func (pm *Manager) CollectPoolStat() telemetry.PoolStat { return ps } -// GetActiveFSManagers returns a list of active filesystem managers. -func (pm *Manager) GetActiveFSManagers() []FSManager { +// GetAvailableFSManagers returns a list of non-empty (active or refreshing) filesystem managers. +func (pm *Manager) GetAvailableFSManagers() []FSManager { fs := []FSManager{} pm.mu.Lock() for _, fsManager := range pm.fsManagerPool { - if pool := fsManager.Pool(); pool != nil && pool.Status() == resources.ActivePool { + if pool := fsManager.Pool(); pool != nil && pool.Status() != resources.EmptyPool { fs = append(fs, fsManager) } } diff --git a/engine/internal/provision/thinclones/lvm/lvmanager.go b/engine/internal/provision/thinclones/lvm/lvmanager.go index fbe50d08bb8d7a9a13f9b0261d6b5dfb1145be0d..f683f893b160736b4e15016a0da19eae2fb6e2c0 100644 --- a/engine/internal/provision/thinclones/lvm/lvmanager.go +++ b/engine/internal/provision/thinclones/lvm/lvmanager.go @@ -106,8 +106,8 @@ func (m *LVManager) CleanupSnapshots(_ int) ([]string, error) { return nil, nil } -// GetSnapshots is not implemented. -func (m *LVManager) GetSnapshots() ([]resources.Snapshot, error) { +// SnapshotList is not implemented. +func (m *LVManager) SnapshotList() []resources.Snapshot { // TODO(anatoly): Not supported in LVM mode warning. return []resources.Snapshot{ { @@ -116,7 +116,12 @@ func (m *LVManager) GetSnapshots() ([]resources.Snapshot, error) { DataStateAt: time.Now(), Pool: m.pool.Name, }, - }, nil + } +} + +// RefreshSnapshotList is not supported in LVM mode. +func (m *LVManager) RefreshSnapshotList() { + log.Msg("RefreshSnapshotList is not supported for LVM. Skip the operation") } // GetSessionState is not implemented. diff --git a/engine/internal/provision/thinclones/zfs/zfs.go b/engine/internal/provision/thinclones/zfs/zfs.go index bf826c647237df80249f3112bb116bb36cd4de5d..1f4e789fee6bf6f3a8dcf11a5ecb94f965a2fb5f 100644 --- a/engine/internal/provision/thinclones/zfs/zfs.go +++ b/engine/internal/provision/thinclones/zfs/zfs.go @@ -10,6 +10,7 @@ import ( "path" "strconv" "strings" + "sync" "time" "unicode" @@ -142,8 +143,10 @@ func (e *EmptyPoolError) Error() string { // Manager describes a filesystem manager for ZFS. type Manager struct { - runner runners.Runner - config Config + runner runners.Runner + config Config + mu *sync.Mutex + snapshots []resources.Snapshot } // Config defines configuration for ZFS filesystem manager. @@ -158,6 +161,7 @@ func NewFSManager(runner runners.Runner, config Config) *Manager { m := Manager{ runner: runner, config: config, + mu: &sync.Mutex{}, } return &m @@ -307,6 +311,20 @@ func (m *Manager) CreateSnapshot(poolSuffix, dataStateAt string) (string, error) } } + dataStateTime, err := util.ParseCustomTime(strings.TrimSuffix(dataStateAt, m.config.PreSnapshotSuffix)) + if err != nil { + return "", fmt.Errorf("failed to parse dataStateAt: %w", err) + } + + m.addSnapshotToList(resources.Snapshot{ + ID: snapshotName, + CreatedAt: time.Now(), + DataStateAt: dataStateTime, + Pool: m.config.Pool.Name, + }) + + go m.RefreshSnapshotList() + return snapshotName, nil } @@ -334,6 +352,8 @@ func (m *Manager) DestroySnapshot(snapshotName string) error { return errors.Wrap(err, "failed to run command") } + m.removeSnapshotFromList(snapshotName) + return nil } @@ -360,6 +380,8 @@ func (m *Manager) CleanupSnapshots(retentionLimit int) ([]string, error) { lines := strings.Split(out, "\n") + m.RefreshSnapshotList() + return lines, nil } @@ -487,8 +509,26 @@ func (m *Manager) GetFilesystemState() (models.FileSystem, error) { return fileSystem, nil } -// GetSnapshots returns a snapshot list. -func (m *Manager) GetSnapshots() ([]resources.Snapshot, error) { +// SnapshotList returns a list of snapshots. +func (m *Manager) SnapshotList() []resources.Snapshot { + snapshotList := m.snapshots + return snapshotList +} + +// RefreshSnapshotList updates the list of snapshots. +func (m *Manager) RefreshSnapshotList() { + snapshots, err := m.getSnapshots() + if err != nil { + log.Err("Failed to refresh snapshot list: ", err) + return + } + + m.mu.Lock() + m.snapshots = snapshots + m.mu.Unlock() +} + +func (m *Manager) getSnapshots() ([]resources.Snapshot, error) { entries, err := m.listSnapshots(m.config.Pool.Name) if err != nil { return nil, fmt.Errorf("failed to list snapshots: %w", err) @@ -517,6 +557,24 @@ func (m *Manager) GetSnapshots() ([]resources.Snapshot, error) { return snapshots, nil } +func (m *Manager) addSnapshotToList(snapshot resources.Snapshot) { + m.mu.Lock() + m.snapshots = append(m.snapshots, snapshot) + m.mu.Unlock() +} + +func (m *Manager) removeSnapshotFromList(snapshotName string) { + for i, snapshot := range m.snapshots { + if snapshot.ID == snapshotName { + m.mu.Lock() + m.snapshots = append(m.snapshots[:i], m.snapshots[i+1:]...) + m.mu.Unlock() + + break + } + } +} + // ListFilesystems lists ZFS file systems (clones, pools). func (m *Manager) listFilesystems(pool string) ([]*ListEntry, error) { filter := snapshotFilter{ diff --git a/engine/internal/provision/thinclones/zfs/zfs_test.go b/engine/internal/provision/thinclones/zfs/zfs_test.go index be23d52f8fcb0cffee0bbb9fde4b9a1aeb601a1c..1b2611f683324757fa2d7efef194d6f7177c12c3 100644 --- a/engine/internal/provision/thinclones/zfs/zfs_test.go +++ b/engine/internal/provision/thinclones/zfs/zfs_test.go @@ -196,3 +196,31 @@ func TestBuildingCommands(t *testing.T) { require.Equal(t, "zfs get -H -p -o value used testSnapshot", command) }) } + +func TestSnapshotList(t *testing.T) { + t.Run("Snapshot list", func(t *testing.T) { + fsManager := NewFSManager(runnerMock{}, Config{}) + + require.Equal(t, 0, len(fsManager.SnapshotList())) + + snapshot := resources.Snapshot{ID: "test1"} + fsManager.addSnapshotToList(snapshot) + + require.Equal(t, 1, len(fsManager.SnapshotList())) + require.Equal(t, []resources.Snapshot{{ID: "test1"}}, fsManager.SnapshotList()) + + snapshot2 := resources.Snapshot{ID: "test2"} + fsManager.addSnapshotToList(snapshot2) + + snapshot3 := resources.Snapshot{ID: "test3"} + fsManager.addSnapshotToList(snapshot3) + + require.Equal(t, 3, len(fsManager.SnapshotList())) + require.Equal(t, []resources.Snapshot{{ID: "test1"}, {ID: "test2"}, {ID: "test3"}}, fsManager.SnapshotList()) + + fsManager.removeSnapshotFromList("test2") + + require.Equal(t, 2, len(fsManager.SnapshotList())) + require.Equal(t, []resources.Snapshot{{ID: "test1"}, {ID: "test3"}}, fsManager.SnapshotList()) + }) +} diff --git a/engine/internal/retrieval/retrieval.go b/engine/internal/retrieval/retrieval.go index f40f97b47bc887d7cc93e29a8c722dc58552f468..245f2250dc7aa69388138c9613c37df0f81530c1 100644 --- a/engine/internal/retrieval/retrieval.go +++ b/engine/internal/retrieval/retrieval.go @@ -22,7 +22,6 @@ import ( "gitlab.com/postgres-ai/database-lab/v3/internal/provision/pool" "gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources" "gitlab.com/postgres-ai/database-lab/v3/internal/provision/runners" - "gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones/zfs" "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/components" "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/config" "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/dbmarker" @@ -525,14 +524,12 @@ func preparePoolToRefresh(poolToUpdate pool.FSManager) error { strings.Join(cloneList, " ")) } - snapshots, err := poolToUpdate.GetSnapshots() - if err != nil { - var emptyErr *zfs.EmptyPoolError - if !errors.As(err, &emptyErr) { - return errors.Wrap(err, "failed to check existing snapshots") - } + poolToUpdate.RefreshSnapshotList() - log.Msg(emptyErr.Error()) + snapshots := poolToUpdate.SnapshotList() + if len(snapshots) == 0 { + log.Msg(fmt.Sprintf("no snapshots for pool %s", poolToUpdate.Pool().Name)) + return nil } for _, snapshotEntry := range snapshots { diff --git a/engine/test/1.synthetic.sh b/engine/test/1.synthetic.sh index 1bb336c83258e07eabac1759ceeab8c5b370ba03..4af6d284d80f8fda25658d843a5f0a0317521863 100644 --- a/engine/test/1.synthetic.sh +++ b/engine/test/1.synthetic.sh @@ -151,6 +151,10 @@ dblab init \ # Check the configuration by fetching the status of the instance: dblab instance status +# Check the snapshot list + if [[ $(dblab snapshot list | jq length) -eq 0 ]] ; then + echo "No snapshot found" && exit 1 + fi ## Create a clone dblab clone create \