Skip to content

Commit 7a6f647

Browse files
committed
Merge branch 'fix/clone-time-skip-redundant-chown' into 'master'
perf(provision): skip redundant recursive chown on clone creation Closes #737 See merge request postgres-ai/database-lab!1169
2 parents 47b647d + d2388b9 commit 7a6f647

8 files changed

Lines changed: 81 additions & 2 deletions

File tree

engine/internal/provision/mode_local_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ func (m mockFSManager) ListClonesNames() ([]string, error) {
8787
return m.cloneList, nil
8888
}
8989

90+
func (m mockFSManager) EnsureDataOwnership(_ string) error {
91+
return nil
92+
}
93+
9094
func (m mockFSManager) CreateSnapshot(_, _ string) (snapshotName string, err error) {
9195
return "", nil
9296
}

engine/internal/provision/pool/manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type StateReporter interface {
4545

4646
// Snapshotter describes methods of snapshot management.
4747
type Snapshotter interface {
48+
EnsureDataOwnership(dataDir string) error
4849
CreateSnapshot(poolSuffix, dataStateAt string) (snapshotName string, err error)
4950
DestroySnapshot(snapshotName string, options thinclones.DestroyOptions) (err error)
5051
CleanupSnapshots(retentionLimit int, mode models.RetrievalMode) ([]string, error)

engine/internal/provision/pool/pool_manager_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func (m *mockFSManager) GetBatchSessionState(_ []resources.SessionStateRequest)
3737
return nil, nil
3838
}
3939
func (m *mockFSManager) GetFilesystemState() (models.FileSystem, error) { return m.filesystem, m.fsErr }
40+
func (m *mockFSManager) EnsureDataOwnership(_ string) error { return nil }
4041
func (m *mockFSManager) CreateSnapshot(_, _ string) (string, error) { return "", nil }
4142
func (m *mockFSManager) DestroySnapshot(_ string, _ thinclones.DestroyOptions) error { return nil }
4243
func (m *mockFSManager) CleanupSnapshots(_ int, _ models.RetrievalMode) ([]string, error) {

engine/internal/provision/thinclones/lvm/lvmanager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ func (m *LVManager) parsePool() error {
9191
return nil
9292
}
9393

94+
// EnsureDataOwnership is a no-op in LVM mode: volume ownership is managed at the volume level.
95+
func (m *LVManager) EnsureDataOwnership(_ string) error {
96+
return nil
97+
}
98+
9499
// CreateSnapshot is not supported in LVM mode.
95100
func (m *LVManager) CreateSnapshot(_, _ string) (string, error) {
96101
log.Msg("Creating a snapshot is not supported in LVM mode. Skip the operation.")

engine/internal/provision/thinclones/zfs/zfs.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,11 @@ func (m *Manager) CreateClone(branchName, cloneName, snapshotID string, revision
207207
}
208208

209209
cloneMountLocation := m.config.Pool.CloneLocation(branchName, cloneName, revision)
210+
cloneDataDir := m.config.Pool.ClonePath(branchName, cloneName, revision)
210211

211-
cmd := fmt.Sprintf("zfs clone -p -o mountpoint=%s %s %s && chown -R %s %s",
212-
cloneMountLocation, snapshotID, cloneMountName, m.config.OSUsername, cloneMountLocation)
212+
cmd := fmt.Sprintf("zfs clone -p -o mountpoint=%s %s %s && %s",
213+
cloneMountLocation, snapshotID, cloneMountName,
214+
chownCloneCommand(cloneDataDir, cloneMountLocation, m.config.OSUsername))
213215

214216
log.Dbg(cmd)
215217

@@ -221,6 +223,49 @@ func (m *Manager) CreateClone(branchName, cloneName, snapshotID string, revision
221223
return nil
222224
}
223225

226+
// EnsureDataOwnership makes dataDir and its contents owned by the engine OS user, walking the tree
227+
// only when it is not already owned by that user. Running it once while preparing a snapshot lets
228+
// every clone created from that snapshot inherit correct ownership and skip the recursive walk.
229+
func (m *Manager) EnsureDataOwnership(dataDir string) error {
230+
cmd := ensureOwnershipCommand(dataDir, m.config.OSUsername)
231+
232+
log.Dbg(cmd)
233+
234+
out, err := m.runner.Run(cmd)
235+
if err != nil {
236+
return errors.Wrapf(err, "failed to ensure data ownership. Out: %v", out)
237+
}
238+
239+
return nil
240+
}
241+
242+
// ownedByUserCondition returns a shell test that succeeds when path is owned by osUsername. The
243+
// compared uid derives from osUsername, so the probe never disagrees with the chown that acts on it,
244+
// and it runs through the same runner as the operation it guards. A failed uid lookup falls back to a
245+
// non-numeric sentinel so that an unresolvable user never matches a missing path (empty == empty).
246+
func ownedByUserCondition(path, osUsername string) string {
247+
return fmt.Sprintf("[ \"$(stat -c '%%u' '%s' 2>/dev/null)\" = \"$(id -u '%s' 2>/dev/null || echo nouid)\" ]",
248+
path, osUsername)
249+
}
250+
251+
// chownCloneCommand builds the ownership step for a freshly created clone. A ZFS clone inherits file
252+
// ownership from its origin snapshot, so when the clone data directory is already owned by the engine
253+
// OS user the expensive recursive walk over the whole data directory is skipped and only the mount
254+
// directory is adjusted; otherwise ownership is applied recursively. The data directory owner is a
255+
// sound probe because every ownership change in the pipeline is applied recursively (uniform owner)
256+
// and PostgreSQL gates startup on the data directory owner.
257+
func chownCloneCommand(dataDir, mountLocation, osUsername string) string {
258+
return fmt.Sprintf("if %s; then chown '%s' '%s'; else chown -R '%s' '%s'; fi",
259+
ownedByUserCondition(dataDir, osUsername), osUsername, mountLocation, osUsername, mountLocation)
260+
}
261+
262+
// ensureOwnershipCommand builds a command that recursively assigns ownership of dataDir to osUsername
263+
// only when it is not already owned by that user.
264+
func ensureOwnershipCommand(dataDir, osUsername string) string {
265+
return fmt.Sprintf("if ! %s; then chown -R '%s' '%s'; fi",
266+
ownedByUserCondition(dataDir, osUsername), osUsername, dataDir)
267+
}
268+
224269
// DestroyClone destroys a ZFS clone.
225270
func (m *Manager) DestroyClone(branchName, cloneName string, revision int) error {
226271
cloneMountName := m.config.Pool.CloneName(branchName, cloneName, revision)

engine/internal/provision/thinclones/zfs/zfs_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,19 @@ func TestBuildingCommands(t *testing.T) {
292292
command := buildSizeCommand("testSnapshot")
293293
require.Equal(t, "zfs get -H -p -o value used testSnapshot", command)
294294
})
295+
296+
t.Run("chown clone command", func(t *testing.T) {
297+
command := chownCloneCommand("/mnt/pool/branch/main/c1/r0/data", "/mnt/pool/branch/main/c1/r0", "dblab")
298+
require.Equal(t, "if [ \"$(stat -c '%u' '/mnt/pool/branch/main/c1/r0/data' 2>/dev/null)\" = "+
299+
"\"$(id -u 'dblab' 2>/dev/null || echo nouid)\" ]; then chown 'dblab' '/mnt/pool/branch/main/c1/r0'; "+
300+
"else chown -R 'dblab' '/mnt/pool/branch/main/c1/r0'; fi", command)
301+
})
302+
303+
t.Run("ensure ownership command", func(t *testing.T) {
304+
command := ensureOwnershipCommand("/mnt/pool/data", "dblab")
305+
require.Equal(t, "if ! [ \"$(stat -c '%u' '/mnt/pool/data' 2>/dev/null)\" = "+
306+
"\"$(id -u 'dblab' 2>/dev/null || echo nouid)\" ]; then chown -R 'dblab' '/mnt/pool/data'; fi", command)
307+
})
295308
}
296309

297310
func TestSnapshotList(t *testing.T) {

engine/internal/retrieval/engine/postgres/snapshot/logical.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ func (s *LogicalInitial) Run(ctx context.Context) error {
165165

166166
dataStateAt := extractDataStateAt(s.dbMarker)
167167

168+
// normalize ownership of the prepared data so clones inherit it and skip the per-clone chown.
169+
if err := s.cloneManager.EnsureDataOwnership(dataDir); err != nil {
170+
return errors.Wrap(err, "failed to ensure data ownership")
171+
}
172+
168173
if _, err := s.cloneManager.CreateSnapshot("", dataStateAt); err != nil {
169174
var existsError *thinclones.SnapshotExistsError
170175
if errors.As(err, &existsError) {

engine/internal/retrieval/engine/postgres/snapshot/physical.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,11 @@ func (p *PhysicalInitial) run(ctx context.Context) (err error) {
412412
return errors.Wrap(err, "failed to mark the prepared data")
413413
}
414414

415+
// normalize ownership of the prepared data so clones inherit it and skip the per-clone chown.
416+
if err := p.cloneManager.EnsureDataOwnership(cloneDataDir); err != nil {
417+
return errors.Wrap(err, "failed to ensure data ownership")
418+
}
419+
415420
// Create a snapshot.
416421
fullClonePath := path.Join(branching.BranchDir, branching.DefaultBranch, cloneName, branching.RevisionSegment(branching.DefaultRevision))
417422
if _, err := p.cloneManager.CreateSnapshot(fullClonePath, p.dbMark.DataStateAt); err != nil {

0 commit comments

Comments
 (0)