github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/disk_journal.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "reflect" 12 "strconv" 13 14 "github.com/keybase/client/go/kbfs/ioutil" 15 "github.com/keybase/client/go/kbfs/kbfscodec" 16 "github.com/pkg/errors" 17 ) 18 19 // journalOrdinal is the ordinal used for naming journal entries. 20 type journalOrdinal uint64 21 22 // TODO: Define the zero journalOrdinal as invalid, once no existing 23 // journals use them. 24 25 const firstValidJournalOrdinal journalOrdinal = 1 26 27 func makeJournalOrdinal(s string) (journalOrdinal, error) { 28 if len(s) != 16 { 29 return 0, errors.Errorf("invalid journal ordinal %q", s) 30 } 31 u, err := strconv.ParseUint(s, 16, 64) 32 if err != nil { 33 return 0, errors.Wrapf(err, "failed to parse %q", s) 34 } 35 return journalOrdinal(u), nil 36 } 37 38 func (o journalOrdinal) String() string { 39 return fmt.Sprintf("%016x", uint64(o)) 40 } 41 42 // diskJournal stores an ordered list of entries in a directory, which 43 // is assumed to not be used by anything else. 44 // 45 // The directory layout looks like: 46 // 47 // dir/EARLIEST 48 // dir/LATEST 49 // dir/0...000 50 // dir/0...001 51 // dir/0...fff 52 // 53 // Each file in dir is named with an ordinal and contains a generic 54 // serializable entry object. The files EARLIEST and LATEST point to 55 // the earliest and latest valid ordinal, respectively. 56 // 57 // This class is not goroutine-safe; it assumes that all 58 // synchronization is done at a higher level. 59 // 60 // TODO: Do all high-level operations atomically on the file-system 61 // level. 62 // 63 // TODO: Make IO ops cancellable. 64 type diskJournal struct { 65 codec kbfscodec.Codec 66 dir string 67 entryType reflect.Type 68 69 // The journal must be considered empty when either 70 // earliestValid or latestValid is false. 71 72 earliestValid bool 73 earliest journalOrdinal 74 75 latestValid bool 76 latest journalOrdinal 77 } 78 79 // makeDiskJournal returns a new diskJournal for the given directory. 80 func makeDiskJournal( 81 codec kbfscodec.Codec, dir string, entryType reflect.Type) ( 82 *diskJournal, error) { 83 j := &diskJournal{ 84 codec: codec, 85 dir: dir, 86 entryType: entryType, 87 } 88 89 earliest, err := j.readEarliestOrdinalFromDisk() 90 switch { 91 case ioutil.IsNotExist(err): 92 // Continue with j.earliestValid = false. 93 case err != nil: 94 return nil, err 95 default: 96 j.earliestValid = true 97 j.earliest = earliest 98 } 99 100 latest, err := j.readLatestOrdinalFromDisk() 101 switch { 102 case ioutil.IsNotExist(err): 103 // Continue with j.latestValid = false. 104 case err != nil: 105 return nil, err 106 default: 107 j.latestValid = true 108 j.latest = latest 109 } 110 111 return j, nil 112 } 113 114 // The functions below are for building various paths for the journal. 115 116 func (j diskJournal) earliestPath() string { 117 return filepath.Join(j.dir, "EARLIEST") 118 } 119 120 func (j diskJournal) latestPath() string { 121 return filepath.Join(j.dir, "LATEST") 122 } 123 124 func (j diskJournal) journalEntryPath(o journalOrdinal) string { 125 return filepath.Join(j.dir, o.String()) 126 } 127 128 // The functions below are for reading and writing the earliest and 129 // latest ordinals. The read functions may return an error for which 130 // ioutil.IsNotExist() returns true. 131 132 func (j diskJournal) readOrdinalFromDisk(path string) (journalOrdinal, error) { 133 buf, err := ioutil.ReadFile(path) 134 if err != nil { 135 return 0, err 136 } 137 return makeJournalOrdinal(string(buf)) 138 } 139 140 func (j *diskJournal) writeOrdinalToDisk(path string, o journalOrdinal) error { 141 return ioutil.WriteSerializedFile(path, []byte(o.String()), 0600) 142 } 143 144 func (j diskJournal) readEarliestOrdinalFromDisk() (journalOrdinal, error) { 145 return j.readOrdinalFromDisk(j.earliestPath()) 146 } 147 148 func (j diskJournal) readLatestOrdinalFromDisk() (journalOrdinal, error) { 149 return j.readOrdinalFromDisk(j.latestPath()) 150 } 151 152 func (j diskJournal) empty() bool { 153 return !j.earliestValid || !j.latestValid 154 } 155 156 // TODO: Change {read,write}{Earliest,Latest}Ordinal() to 157 // {get,set}{Earliest,Latest}Ordinal(), and have the getters return an 158 // isValid bool, or an invalid journalOrdinal instead of an error. 159 160 func (j diskJournal) readEarliestOrdinal() (journalOrdinal, error) { 161 if j.empty() { 162 return journalOrdinal(0), errors.WithStack(os.ErrNotExist) 163 } 164 return j.earliest, nil 165 } 166 167 func (j *diskJournal) writeEarliestOrdinal(o journalOrdinal) error { 168 err := j.writeOrdinalToDisk(j.earliestPath(), o) 169 if err != nil { 170 return err 171 } 172 j.earliestValid = true 173 j.earliest = o 174 return nil 175 } 176 177 func (j diskJournal) readLatestOrdinal() (journalOrdinal, error) { 178 if j.empty() { 179 return journalOrdinal(0), errors.WithStack(os.ErrNotExist) 180 } 181 return j.latest, nil 182 } 183 184 func (j *diskJournal) writeLatestOrdinal(o journalOrdinal) error { 185 err := j.writeOrdinalToDisk(j.latestPath(), o) 186 if err != nil { 187 return err 188 } 189 j.latestValid = true 190 j.latest = o 191 return nil 192 } 193 194 // clear completely removes the journal directory. 195 func (j *diskJournal) clear() error { 196 // Clear ordinals first to not leave the journal in a weird 197 // state if we crash in the middle of removing the files, 198 // assuming that file removal is atomic. 199 err := ioutil.Remove(j.earliestPath()) 200 if err != nil { 201 return err 202 } 203 204 // If we crash here, on the next startup the journal will 205 // still be considered empty. 206 207 j.earliestValid = false 208 j.earliest = journalOrdinal(0) 209 210 err = ioutil.Remove(j.latestPath()) 211 if err != nil { 212 return err 213 } 214 215 j.latestValid = false 216 j.latest = journalOrdinal(0) 217 218 // j.dir will be recreated on the next call to 219 // writeJournalEntry (via kbfscodec.SerializeToFile), which 220 // must always come before any ordinal write. 221 return ioutil.RemoveAll(j.dir) 222 } 223 224 // removeEarliest removes the earliest entry in the journal. If that 225 // entry was the last one, clear() is also called, and true is 226 // returned. 227 func (j *diskJournal) removeEarliest() (empty bool, err error) { 228 if j.empty() { 229 // TODO: Return a more meaningful error. 230 return false, errors.WithStack(os.ErrNotExist) 231 } 232 233 if j.earliest == j.latest { 234 err := j.clear() 235 if err != nil { 236 return false, err 237 } 238 return true, nil 239 } 240 241 oldEarliest := j.earliest 242 243 err = j.writeEarliestOrdinal(oldEarliest + 1) 244 if err != nil { 245 return false, err 246 } 247 248 // Garbage-collect the old entry. If we crash here and leave 249 // behind an entry, it'll be cleaned up the next time clear() 250 // is called. 251 p := j.journalEntryPath(oldEarliest) 252 err = ioutil.Remove(p) 253 if err != nil { 254 return false, err 255 } 256 257 return false, nil 258 } 259 260 // The functions below are for reading and writing journal entries. 261 262 func (j diskJournal) readJournalEntry(o journalOrdinal) (interface{}, error) { 263 p := j.journalEntryPath(o) 264 entry := reflect.New(j.entryType) 265 err := kbfscodec.DeserializeFromFile(j.codec, p, entry) 266 if err != nil { 267 return nil, err 268 } 269 270 return entry.Elem().Interface(), nil 271 } 272 273 func (j *diskJournal) writeJournalEntry( 274 o journalOrdinal, entry interface{}) error { 275 entryType := reflect.TypeOf(entry) 276 if entryType != j.entryType { 277 panic(errors.Errorf("Expected entry type %v, got %v", 278 j.entryType, entryType)) 279 } 280 281 return kbfscodec.SerializeToFile(j.codec, entry, j.journalEntryPath(o)) 282 } 283 284 // appendJournalEntry appends the given entry to the journal. If o is 285 // nil, then if the journal is empty, the new entry will have ordinal 286 // 0, and otherwise it will have ordinal equal to the successor of the 287 // latest ordinal. Otherwise, if o is non-nil, then if the journal is 288 // empty, the new entry will have ordinal *o, and otherwise it returns 289 // an error if *o is not the successor of the latest ordinal. If 290 // successful, appendJournalEntry returns the ordinal of the 291 // just-appended entry. 292 func (j *diskJournal) appendJournalEntry( 293 o *journalOrdinal, entry interface{}) (journalOrdinal, error) { 294 var next journalOrdinal 295 if j.empty() { 296 if o != nil { 297 next = *o 298 } else { 299 next = firstValidJournalOrdinal 300 } 301 } else { 302 next = j.latest + 1 303 if next == 0 { 304 // Rollover is almost certainly a bug. 305 return 0, errors.Errorf( 306 "Ordinal rollover for %+v", entry) 307 } 308 if o != nil && next != *o { 309 return 0, errors.Errorf( 310 "%v unexpectedly does not follow %v for %+v", 311 *o, j.latest, entry) 312 } 313 } 314 315 err := j.writeJournalEntry(next, entry) 316 if err != nil { 317 return 0, err 318 } 319 320 if j.empty() { 321 err := j.writeEarliestOrdinal(next) 322 if err != nil { 323 return 0, err 324 } 325 } 326 err = j.writeLatestOrdinal(next) 327 if err != nil { 328 return 0, err 329 } 330 return next, nil 331 } 332 333 // move moves the journal to the given directory, which should share 334 // the same parent directory as the current journal directory. 335 func (j *diskJournal) move(newDir string) (oldDir string, err error) { 336 err = ioutil.Rename(j.dir, newDir) 337 if err != nil && !ioutil.IsNotExist(err) { 338 return "", err 339 } 340 oldDir = j.dir 341 j.dir = newDir 342 return oldDir, nil 343 } 344 345 func (j diskJournal) length() uint64 { 346 if j.empty() { 347 return 0 348 } 349 return uint64(j.latest - j.earliest + 1) 350 }