dir_plan9.go raw
1 /*
2 * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6 package badger
7
8 import (
9 "fmt"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/dgraph-io/badger/v4/y"
15 )
16
17 // directoryLockGuard holds a lock on a directory and a pid file inside. The pid file isn't part
18 // of the locking mechanism, it's just advisory.
19 type directoryLockGuard struct {
20 // File handle on the directory, which we've locked.
21 f *os.File
22 // The absolute path to our pid file.
23 path string
24 }
25
26 // acquireDirectoryLock gets a lock on the directory.
27 // It will also write our pid to dirPath/pidFileName for convenience.
28 // readOnly is not supported on Plan 9.
29 func acquireDirectoryLock(dirPath string, pidFileName string, readOnly bool) (
30 *directoryLockGuard, error) {
31 if readOnly {
32 return nil, ErrPlan9NotSupported
33 }
34
35 // Convert to absolute path so that Release still works even if we do an unbalanced
36 // chdir in the meantime.
37 absPidFilePath, err := filepath.Abs(filepath.Join(dirPath, pidFileName))
38 if err != nil {
39 return nil, y.Wrap(err, "cannot get absolute path for pid lock file")
40 }
41
42 // If the file was unpacked or created by some other program, it might not
43 // have the ModeExclusive bit set. Set it before we call OpenFile, so that we
44 // can be confident that a successful OpenFile implies exclusive use.
45 //
46 // OpenFile fails if the file ModeExclusive bit set *and* the file is already open.
47 // So, if the file is closed when the DB crashed, we're fine. When the process
48 // that was managing the DB crashes, the OS will close the file for us.
49 //
50 // This bit of code is copied from Go's lockedfile internal package:
51 // https://github.com/golang/go/blob/go1.15rc1/src/cmd/go/internal/lockedfile/lockedfile_plan9.go#L58
52 if fi, err := os.Stat(absPidFilePath); err == nil {
53 if fi.Mode()&os.ModeExclusive == 0 {
54 if err := os.Chmod(absPidFilePath, fi.Mode()|os.ModeExclusive); err != nil {
55 return nil, y.Wrapf(err, "could not set exclusive mode bit")
56 }
57 }
58 } else if !os.IsNotExist(err) {
59 return nil, err
60 }
61 f, err := os.OpenFile(absPidFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666|os.ModeExclusive)
62 if err != nil {
63 if isLocked(err) {
64 return nil, y.Wrapf(err,
65 "Cannot open pid lock file %q. Another process is using this Badger database",
66 absPidFilePath)
67 }
68 return nil, y.Wrapf(err, "Cannot open pid lock file %q", absPidFilePath)
69 }
70
71 if _, err = fmt.Fprintf(f, "%d\n", os.Getpid()); err != nil {
72 f.Close()
73 return nil, y.Wrapf(err, "could not write pid")
74 }
75 return &directoryLockGuard{f, absPidFilePath}, nil
76 }
77
78 // Release deletes the pid file and releases our lock on the directory.
79 func (guard *directoryLockGuard) release() error {
80 // It's important that we remove the pid file first.
81 err := os.Remove(guard.path)
82
83 if closeErr := guard.f.Close(); err == nil {
84 err = closeErr
85 }
86 guard.path = ""
87 guard.f = nil
88
89 return err
90 }
91
92 // openDir opens a directory for syncing.
93 func openDir(path string) (*os.File, error) { return os.Open(path) }
94
95 // When you create or delete a file, you have to ensure the directory entry for the file is synced
96 // in order to guarantee the file is visible (if the system crashes). (See the man page for fsync,
97 // or see https://github.com/coreos/etcd/issues/6368 for an example.)
98 func syncDir(dir string) error {
99 f, err := openDir(dir)
100 if err != nil {
101 return y.Wrapf(err, "While opening directory: %s.", dir)
102 }
103
104 err = f.Sync()
105 closeErr := f.Close()
106 if err != nil {
107 return y.Wrapf(err, "While syncing directory: %s.", dir)
108 }
109 return y.Wrapf(closeErr, "While closing directory: %s.", dir)
110 }
111
112 // Opening an exclusive-use file returns an error.
113 // The expected error strings are:
114 //
115 // - "open/create -- file is locked" (cwfs, kfs)
116 // - "exclusive lock" (fossil)
117 // - "exclusive use file already open" (ramfs)
118 //
119 // See https://github.com/golang/go/blob/go1.15rc1/src/cmd/go/internal/lockedfile/lockedfile_plan9.go#L16
120 var lockedErrStrings = [...]string{
121 "file is locked",
122 "exclusive lock",
123 "exclusive use file already open",
124 }
125
126 // Even though plan9 doesn't support the Lock/RLock/Unlock functions to
127 // manipulate already-open files, IsLocked is still meaningful: os.OpenFile
128 // itself may return errors that indicate that a file with the ModeExclusive bit
129 // set is already open.
130 func isLocked(err error) bool {
131 s := err.Error()
132
133 for _, frag := range lockedErrStrings {
134 if strings.Contains(s, frag) {
135 return true
136 }
137 }
138 return false
139 }
140