├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go └── main_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | test: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Test 20 | run: make test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | txngo 2 | txngo.log 3 | txngo.db 4 | txngo.tmp 5 | tmp/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2019 Kawamura Shintaro 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRCS = main.go 2 | 3 | txngo: 4 | go build -o txngo $(SRCS) 5 | 6 | .PHONY: run 7 | run: 8 | go run $(SRCS) 9 | 10 | .PHONY: run_tcp 11 | run_tcp: 12 | go run $(SRCS) -tcp localhost:3000 13 | 14 | .PHONY: test 15 | test: 16 | go test -v 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -f txngo 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # txngo 2 | 3 | [![Build Status](https://github.com/kawasin73/txngo/workflows/Go/badge.svg)](https://github.com/kawasin73/txngo/actions) 4 | 5 | simple transaction implementation with Go. 6 | 7 | txngo is now based on **S2PL (Strict Two Phase Lock) Concurrency Control** with **MultiThread** and is **In-memory KVS**. 8 | 9 | Key is string (< 255 length) and Value is []byte (< unsigned 32bit interger max size). 10 | 11 | Using Upgradable RWMutex by [github.com/kawasin73/umutex](https://github.com/kawasin73/umutex) internally. 12 | 13 | txngo have interactive interface via stdio or tcp. 14 | 15 | ## Features 16 | 17 | - Supports `INSERT` `UPDATE` `READ` `DELETE` `COMMIT` `ABORT` operations. 18 | - WAL (Write Ahead Log) 19 | - Only have Redo log and write all logs at commit phase 20 | - Checkpoint 21 | - write back data only when shutdown 22 | - Crash Recovery 23 | - Redo log have idempotency. 24 | - Interactive Interface using stdin and stdout or tcp connection 25 | 26 | ## Example 27 | 28 | ```bash 29 | $ txngo 30 | # or 31 | $ cd path/to/txngo && make run 32 | GOMAXPROCS=1 go run main.go 33 | 2019/09/24 20:14:44 loading data file... 34 | 2019/09/24 20:14:44 db file is not found. this is initial start. 35 | 2019/09/24 20:14:44 loading WAL file... 36 | >> insert key1 value1 37 | success to insert "key1" 38 | >> insert key2 value2 39 | success to insert "key2" 40 | >> insert key3 value3 41 | success to insert "key3" 42 | >> read key1 43 | value1 44 | >> commit 45 | committed 46 | >> read key1 47 | value1 48 | >> delete key2 49 | success to delete "key2" 50 | >> update key3 value4 51 | success to update "key3" 52 | >> commit 53 | committed 54 | >> read key3 55 | value4 56 | >> read key2 57 | failed to read : record not exists 58 | >> delete key1 59 | success to delete "key1" 60 | >> abort 61 | aborted 62 | >> keys 63 | >>> show keys commited <<< 64 | key3 65 | key1 66 | >> quit 67 | byebye 68 | 2019/09/24 20:15:56 shutdown... 69 | 2019/09/24 20:15:56 success to save data 70 | $ make run 71 | GOMAXPROCS=1 go run main.go 72 | 2019/09/24 20:15:58 loading data file... 73 | 2019/09/24 20:15:58 loading WAL file... 74 | >> keys 75 | >>> show keys commited <<< 76 | key1 77 | key3 78 | >> read key1 79 | value1 80 | >> read key3 81 | value4 82 | >> quit 83 | byebye 84 | 2019/09/24 20:16:11 shutdown... 85 | 2019/09/24 20:16:11 success to save data 86 | ``` 87 | 88 | ## Options 89 | 90 | ```bash 91 | $ ./txngo -h 92 | Usage of ./txngo: 93 | -db string 94 | file path of data file (default "./txngo.db") 95 | -init 96 | create data file if not exist (default true) 97 | -tcp string 98 | tcp handler address (e.g. localhost:3000) 99 | -wal string 100 | file path of WAL file (default "./txngo.log") 101 | ``` 102 | 103 | ## How to 104 | 105 | ### Installation 106 | 107 | ```bash 108 | $ go get https://github.com/kawasin73/txngo.git 109 | ``` 110 | 111 | ### Test 112 | 113 | ```bash 114 | $ make test 115 | ``` 116 | 117 | ## LICENSE 118 | 119 | MIT 120 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kawasin73/txngo 2 | 3 | go 1.13 4 | 5 | require github.com/kawasin73/umutex v0.2.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kawasin73/umutex v0.1.0 h1:2tqjaiY8NVRMWQSQOJbDAc96tSXsZwdlVySMr/+2C10= 2 | github.com/kawasin73/umutex v0.1.0/go.mod h1:A02N2muKVFMvFlp5c+hBycgdH964YtieGs+7mYB16NU= 3 | github.com/kawasin73/umutex v0.2.1 h1:Onkzz3LKs1HThskVwdhhBocqdRQqwCZ03quDJzuPzPo= 4 | github.com/kawasin73/umutex v0.2.1/go.mod h1:A02N2muKVFMvFlp5c+hBycgdH964YtieGs+7mYB16NU= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "hash/crc32" 10 | "io" 11 | "log" 12 | "net" 13 | "os" 14 | "os/signal" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/kawasin73/umutex" 20 | ) 21 | 22 | const ( 23 | LInsert = 1 + iota 24 | LDelete 25 | LUpdate 26 | LRead 27 | LCommit 28 | LAbort 29 | ) 30 | 31 | var ( 32 | ErrExist = errors.New("record already exists") 33 | ErrNotExist = errors.New("record not exists") 34 | ErrBufferShort = errors.New("buffer size is not enough to deserialize") 35 | ErrChecksum = errors.New("checksum does not match") 36 | ErrDeadLock = errors.New("deadlock detected") 37 | ) 38 | 39 | type Record struct { 40 | Key string 41 | Value []byte 42 | } 43 | 44 | func (r *Record) Serialize(buf []byte) (int, error) { 45 | key := []byte(r.Key) 46 | value := r.Value 47 | total := 5 + len(key) + len(value) 48 | 49 | // check buffer size 50 | if len(buf) < total { 51 | return 0, ErrBufferShort 52 | } 53 | 54 | // serialize 55 | // TODO: support NULL value 56 | buf[0] = uint8(len(key)) 57 | binary.BigEndian.PutUint32(buf[1:], uint32(len(r.Value))) 58 | copy(buf[5:], key) 59 | copy(buf[5+len(key):], r.Value) 60 | 61 | return total, nil 62 | } 63 | 64 | func (r *Record) Deserialize(buf []byte) (int, error) { 65 | if len(buf) < 5 { 66 | return 0, ErrBufferShort 67 | } 68 | 69 | // parse length 70 | keyLen := buf[0] 71 | valueLen := binary.BigEndian.Uint32(buf[1:]) 72 | total := 5 + int(keyLen) + int(valueLen) 73 | if len(buf) < total { 74 | return 0, ErrBufferShort 75 | } 76 | 77 | // copy key and value from buffer 78 | r.Key = string(buf[5 : 5+keyLen]) 79 | // TODO: support NULL value 80 | r.Value = make([]byte, valueLen) 81 | copy(r.Value, buf[5+keyLen:total]) 82 | 83 | return total, nil 84 | } 85 | 86 | type RecordLog struct { 87 | Action uint8 88 | Record 89 | } 90 | 91 | func (r *RecordLog) Serialize(buf []byte) (int, error) { 92 | if len(buf) < 5 { 93 | return 0, ErrBufferShort 94 | } 95 | 96 | buf[0] = r.Action 97 | var total = 1 98 | if r.Action > LRead { 99 | // LCommit or LAbort 100 | } else { 101 | // serialize record content first (check buffer size) 102 | n, err := r.Record.Serialize(buf[1:]) 103 | if err != nil { 104 | return 0, err 105 | } 106 | total += n 107 | } 108 | if len(buf) < total+4 { 109 | return 0, ErrBufferShort 110 | } 111 | 112 | // generate checksum 113 | hash := crc32.NewIEEE() 114 | if _, err := hash.Write(buf[:total]); err != nil { 115 | return 0, err 116 | } 117 | binary.BigEndian.PutUint32(buf[total:], hash.Sum32()) 118 | 119 | return total + 4, nil 120 | } 121 | 122 | func (r *RecordLog) Deserialize(buf []byte) (int, error) { 123 | if len(buf) < 5 { 124 | return 0, ErrBufferShort 125 | } 126 | r.Action = buf[0] 127 | var total = 1 128 | switch r.Action { 129 | case LCommit: 130 | 131 | case LInsert, LUpdate, LDelete: 132 | n, err := r.Record.Deserialize(buf[1:]) 133 | if err != nil { 134 | return 0, err 135 | } 136 | total += n 137 | 138 | default: 139 | return 0, fmt.Errorf("action is not supported : %v", r.Action) 140 | } 141 | 142 | // validate checksum 143 | hash := crc32.NewIEEE() 144 | if _, err := hash.Write(buf[:total]); err != nil { 145 | return 0, err 146 | } 147 | if binary.BigEndian.Uint32(buf[total:]) != hash.Sum32() { 148 | return 0, ErrChecksum 149 | } 150 | 151 | return total + 4, nil 152 | } 153 | 154 | type lock struct { 155 | mu umutex.UMutex 156 | refs int 157 | } 158 | 159 | type Locker struct { 160 | mu sync.Mutex 161 | mutexes map[string]*lock 162 | } 163 | 164 | func NewLocker() *Locker { 165 | return &Locker{ 166 | mutexes: make(map[string]*lock), 167 | } 168 | } 169 | 170 | func (l *Locker) refLock(key string) *lock { 171 | l.mu.Lock() 172 | rec, ok := l.mutexes[key] 173 | if !ok { 174 | // TODO: not create lock object each time, use Pool or preallocate for each record 175 | rec = new(lock) 176 | l.mutexes[key] = rec 177 | } 178 | rec.refs++ 179 | l.mu.Unlock() 180 | return rec 181 | } 182 | 183 | func (l *Locker) unrefLock(key string) *lock { 184 | l.mu.Lock() 185 | rec := l.mutexes[key] 186 | rec.refs-- 187 | if rec.refs == 0 { 188 | delete(l.mutexes, key) 189 | } 190 | l.mu.Unlock() 191 | return rec 192 | } 193 | 194 | func (l *Locker) getLock(key string) *lock { 195 | l.mu.Lock() 196 | rec := l.mutexes[key] 197 | l.mu.Unlock() 198 | return rec 199 | } 200 | 201 | func (l *Locker) Lock(key string) { 202 | rec := l.refLock(key) 203 | rec.mu.Lock() 204 | } 205 | 206 | func (l *Locker) Unlock(key string) { 207 | rec := l.unrefLock(key) 208 | rec.mu.Unlock() 209 | } 210 | 211 | func (l *Locker) RLock(key string) { 212 | rec := l.refLock(key) 213 | rec.mu.RLock() 214 | } 215 | 216 | func (l *Locker) RUnlock(key string) { 217 | rec := l.unrefLock(key) 218 | rec.mu.RUnlock() 219 | } 220 | 221 | func (l *Locker) Upgrade(key string) bool { 222 | rec := l.getLock(key) 223 | return rec.mu.Upgrade() 224 | } 225 | 226 | func (l *Locker) Downgrade(key string) { 227 | rec := l.getLock(key) 228 | rec.mu.Downgrade() 229 | } 230 | 231 | type Storage struct { 232 | muWAL sync.Mutex 233 | muDB sync.RWMutex 234 | dbPath string 235 | tmpPath string 236 | wal *os.File 237 | db map[string]Record 238 | lock *Locker 239 | } 240 | 241 | func NewStorage(wal *os.File, dbPath, tmpPath string) *Storage { 242 | return &Storage{ 243 | dbPath: dbPath, 244 | tmpPath: tmpPath, 245 | wal: wal, 246 | db: make(map[string]Record), 247 | lock: NewLocker(), 248 | } 249 | } 250 | 251 | func (s *Storage) ApplyLogs(logs []RecordLog) { 252 | s.muDB.Lock() 253 | defer s.muDB.Unlock() 254 | // TODO: optimize when duplicate keys in logs 255 | for _, rlog := range logs { 256 | switch rlog.Action { 257 | case LInsert: 258 | s.db[rlog.Key] = rlog.Record 259 | 260 | case LUpdate: 261 | // reuse Key string in db and Key in rlog will be GCed. 262 | r, ok := s.db[rlog.Key] 263 | if !ok { 264 | // record in db may be sometimes deleted. complete with rlog.Key for idempotency. 265 | r.Key = rlog.Key 266 | } 267 | r.Value = rlog.Value 268 | s.db[r.Key] = r 269 | 270 | case LDelete: 271 | delete(s.db, rlog.Key) 272 | } 273 | } 274 | } 275 | 276 | func (s *Storage) SaveWAL(logs []RecordLog) error { 277 | // prevent parallel WAL writing by unexpected context switch 278 | s.muWAL.Lock() 279 | defer s.muWAL.Unlock() 280 | 281 | var ( 282 | i int 283 | buf [4096]byte 284 | ) 285 | 286 | for _, rlog := range logs { 287 | n, err := rlog.Serialize(buf[i:]) 288 | if err == ErrBufferShort { 289 | // TODO: use writev 290 | return err 291 | } else if err != nil { 292 | return err 293 | } 294 | 295 | // TODO: delay write and combine multi log into one buffer 296 | _, err = s.wal.Write(buf[:n]) 297 | if err != nil { 298 | return err 299 | } 300 | } 301 | 302 | // write commit log 303 | n, err := (&RecordLog{Action: LCommit}).Serialize(buf[:]) 304 | if err != nil { 305 | // commit log serialization must not fail 306 | log.Panic(err) 307 | } 308 | _, err = s.wal.Write(buf[:n]) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | // sync this transaction 314 | err = s.wal.Sync() 315 | if err != nil { 316 | return err 317 | } 318 | 319 | return nil 320 | } 321 | 322 | func (s *Storage) LoadWAL() (int, error) { 323 | if _, err := s.wal.Seek(0, io.SeekStart); err != nil { 324 | return 0, err 325 | } 326 | 327 | var ( 328 | logs []RecordLog 329 | buf [4096]byte 330 | head int 331 | size int 332 | nlogs int 333 | ) 334 | 335 | // redo all record logs in WAL file 336 | for { 337 | var rlog RecordLog 338 | n, err := rlog.Deserialize(buf[head:size]) 339 | if err == ErrBufferShort { 340 | // move data to head 341 | copy(buf[:], buf[head:size]) 342 | size -= head 343 | 344 | if size == 4096 { 345 | // buffer size (4096) is too short for this log 346 | // TODO: allocate and read directly to db buffer 347 | return 0, err 348 | } 349 | 350 | // read more log data to buffer 351 | n, err = s.wal.Read(buf[size:]) 352 | size += n 353 | if err == io.EOF { 354 | break 355 | } else if err != nil { 356 | return 0, err 357 | } 358 | continue 359 | } else if err != nil { 360 | return 0, err 361 | } 362 | head += n 363 | nlogs++ 364 | 365 | switch rlog.Action { 366 | case LInsert, LUpdate, LDelete: 367 | // append log 368 | logs = append(logs, rlog) 369 | 370 | case LCommit: 371 | // redo record logs 372 | s.ApplyLogs(logs) 373 | 374 | // clear logs 375 | logs = nil 376 | 377 | case LAbort: 378 | // clear logs 379 | logs = nil 380 | 381 | default: 382 | // skip 383 | } 384 | } 385 | 386 | return nlogs, nil 387 | } 388 | 389 | func (s *Storage) ClearWAL() error { 390 | if _, err := s.wal.Seek(0, io.SeekStart); err != nil { 391 | return err 392 | } else if err = s.wal.Truncate(0); err != nil { 393 | return err 394 | // it is not obvious that ftruncate(2) sync the change to disk or not. sync explicitly for safe. 395 | } else if err = s.wal.Sync(); err != nil { 396 | return err 397 | } 398 | return nil 399 | } 400 | 401 | func (s *Storage) SaveCheckPoint() error { 402 | // create temporary checkout file 403 | f, err := os.Create(s.tmpPath) 404 | if err != nil { 405 | return err 406 | } 407 | defer f.Close() 408 | 409 | var buf [4096]byte 410 | // write header 411 | binary.BigEndian.PutUint32(buf[:4], uint32(len(s.db))) 412 | _, err = f.Write(buf[:4]) 413 | if err != nil { 414 | goto ERROR 415 | } 416 | 417 | // write all data 418 | for _, r := range s.db { 419 | // FIXME: key order in map will be randomized 420 | n, err := r.Serialize(buf[:]) 421 | if err == ErrBufferShort { 422 | // TODO: use writev 423 | goto ERROR 424 | } else if err != nil { 425 | goto ERROR 426 | } 427 | 428 | // TODO: delay write and combine multi log into one buffer 429 | _, err = f.Write(buf[:n]) 430 | if err != nil { 431 | goto ERROR 432 | } 433 | } 434 | 435 | if err = f.Sync(); err != nil { 436 | goto ERROR 437 | } 438 | 439 | // swap dbfile and temporary file 440 | err = os.Rename(s.tmpPath, s.dbPath) 441 | if err != nil { 442 | goto ERROR 443 | } 444 | 445 | return nil 446 | 447 | ERROR: 448 | if rerr := os.Remove(s.tmpPath); rerr != nil { 449 | log.Println("failed to remove temporary file for checkpoint :", rerr) 450 | } 451 | return err 452 | } 453 | 454 | func (s *Storage) LoadCheckPoint() error { 455 | f, err := os.Open(s.dbPath) 456 | if err != nil { 457 | return err 458 | } 459 | defer f.Close() 460 | 461 | var buf [4096]byte 462 | 463 | // read and parse header 464 | n, err := f.Read(buf[:]) 465 | if err != nil { 466 | return err 467 | } else if n < 4 { 468 | return fmt.Errorf("file header size is too short : %v", n) 469 | } 470 | total := binary.BigEndian.Uint32(buf[:4]) 471 | if total == 0 { 472 | if n == 4 { 473 | return nil 474 | } else { 475 | return fmt.Errorf("total is 0. but db file have some data") 476 | } 477 | } 478 | 479 | var ( 480 | head = 4 481 | size = n 482 | loaded uint32 483 | ) 484 | 485 | // read all data 486 | for { 487 | var r Record 488 | n, err = r.Deserialize(buf[head:size]) 489 | if err == ErrBufferShort { 490 | if size-head == 4096 { 491 | // buffer size (4096) is too short for this log 492 | // TODO: allocate and read directly to db buffer 493 | return err 494 | } 495 | 496 | // move data to head 497 | copy(buf[:], buf[head:size]) 498 | size -= head 499 | 500 | // read more log data to buffer 501 | n, err = f.Read(buf[size:]) 502 | size += n 503 | if err == io.EOF { 504 | break 505 | } else if err != nil { 506 | return err 507 | } 508 | continue 509 | } else if err != nil { 510 | return err 511 | } 512 | 513 | // set data 514 | s.db[r.Key] = r 515 | loaded++ 516 | head += n 517 | 518 | if loaded > total { 519 | // records in checkpoint file is more than specified in header 520 | break 521 | } 522 | } 523 | 524 | if loaded != total { 525 | return fmt.Errorf("db file is broken : total %v records but actually %v records", total, loaded) 526 | } else if size != 0 { 527 | return fmt.Errorf("db file is broken : file size is larger than expected") 528 | } 529 | return nil 530 | } 531 | 532 | type Txn struct { 533 | s *Storage 534 | logs []RecordLog 535 | readSet map[string]*Record 536 | writeSet map[string]int 537 | } 538 | 539 | func (s *Storage) NewTxn() *Txn { 540 | return &Txn{ 541 | s: s, 542 | readSet: make(map[string]*Record), 543 | writeSet: make(map[string]int), 544 | } 545 | } 546 | 547 | func (txn *Txn) Read(key string) ([]byte, error) { 548 | if r, ok := txn.readSet[key]; ok { 549 | if r == nil { 550 | return nil, ErrNotExist 551 | } 552 | return r.Value, nil 553 | } else if idx, ok := txn.writeSet[key]; ok { 554 | rec := txn.logs[idx] 555 | if rec.Action == LDelete { 556 | return nil, ErrNotExist 557 | } 558 | return rec.Value, nil 559 | } 560 | 561 | // read lock 562 | txn.s.lock.RLock(key) 563 | 564 | txn.s.muDB.RLock() 565 | r, ok := txn.s.db[key] 566 | txn.s.muDB.RUnlock() 567 | if !ok { 568 | txn.readSet[key] = nil 569 | return nil, ErrNotExist 570 | } 571 | 572 | txn.readSet[r.Key] = &r 573 | return r.Value, nil 574 | } 575 | 576 | func clone(v []byte) []byte { 577 | // TODO: support NULL value 578 | v2 := make([]byte, len(v)) 579 | copy(v2, v) 580 | return v2 581 | } 582 | 583 | // ensureNotExist check readSet and writeSet step by step that there IS NOT the record. 584 | // This method is used by Insert. 585 | func (txn *Txn) ensureNotExist(key string) (string, error) { 586 | if r, ok := txn.readSet[key]; ok { 587 | if r != nil { 588 | return "", ErrExist 589 | } 590 | // reallocate string 591 | key = string(key) 592 | 593 | if !txn.s.lock.Upgrade(key) { 594 | return "", ErrDeadLock 595 | } 596 | // move record from readSet to writeSet 597 | delete(txn.readSet, key) 598 | } else if idx, ok := txn.writeSet[key]; ok { 599 | rec := txn.logs[idx] 600 | if rec.Action != LDelete { 601 | return "", ErrExist 602 | } 603 | // reuse key in writeSet 604 | key = rec.Key 605 | } else { 606 | // lock record 607 | txn.s.lock.Lock(key) 608 | 609 | // reallocate string 610 | key = string(key) 611 | 612 | // check that the key not exists in db 613 | txn.s.muDB.RLock() 614 | r, ok := txn.s.db[key] 615 | txn.s.muDB.RUnlock() 616 | if ok { 617 | txn.readSet[key] = &r 618 | txn.s.lock.Downgrade(key) 619 | return "", ErrExist 620 | } 621 | } 622 | 623 | return key, nil 624 | } 625 | 626 | // ensureExist check readSet and writeSet step by step that there IS the record. 627 | // This method is used by Update, Delete. 628 | func (txn *Txn) ensureExist(key string) (newKey string, err error) { 629 | if r, ok := txn.readSet[key]; ok { 630 | if r == nil { 631 | return "", ErrNotExist 632 | } 633 | 634 | // reuse key in readSet 635 | key = r.Key 636 | if !txn.s.lock.Upgrade(key) { 637 | return "", ErrDeadLock 638 | } 639 | // move record from readSet to writeSet 640 | delete(txn.readSet, key) 641 | } else if idx, ok := txn.writeSet[key]; ok { 642 | rec := txn.logs[idx] 643 | if rec.Action == LDelete { 644 | return "", ErrNotExist 645 | } 646 | // reuse key in writeSet 647 | key = rec.Key 648 | } else { 649 | // lock record 650 | txn.s.lock.Lock(key) 651 | 652 | // check that the key exists in db 653 | txn.s.muDB.RLock() 654 | r, ok := txn.s.db[key] 655 | txn.s.muDB.RUnlock() 656 | if !ok { 657 | key = string(key) 658 | txn.readSet[key] = nil 659 | txn.s.lock.Downgrade(key) 660 | return "", ErrNotExist 661 | } 662 | // reuse key in db 663 | key = r.Key 664 | } 665 | 666 | return key, nil 667 | } 668 | 669 | func (txn *Txn) Insert(key string, value []byte) error { 670 | key, err := txn.ensureNotExist(key) 671 | if err != nil { 672 | return err 673 | } 674 | 675 | // clone value to prevent injection after transaction 676 | value = clone(value) 677 | 678 | // add insert log 679 | txn.logs = append(txn.logs, RecordLog{ 680 | Action: LInsert, 681 | Record: Record{ 682 | Key: key, 683 | Value: value, 684 | }, 685 | }) 686 | 687 | // add to or update writeSet (index of logs) 688 | txn.writeSet[key] = len(txn.logs) - 1 689 | return nil 690 | } 691 | 692 | func (txn *Txn) Update(key string, value []byte) error { 693 | key, err := txn.ensureExist(key) 694 | if err != nil { 695 | return err 696 | } 697 | 698 | // clone value to prevent injection after transaction 699 | value = clone(value) 700 | 701 | // add update log 702 | txn.logs = append(txn.logs, RecordLog{ 703 | Action: LUpdate, 704 | Record: Record{ 705 | Key: key, 706 | Value: value, 707 | }, 708 | }) 709 | 710 | // add to or update writeSet (index of logs) 711 | txn.writeSet[key] = len(txn.logs) - 1 712 | return nil 713 | } 714 | 715 | func (txn *Txn) Delete(key string) error { 716 | key, err := txn.ensureExist(key) 717 | if err != nil { 718 | return err 719 | } 720 | 721 | // add delete log 722 | txn.logs = append(txn.logs, RecordLog{ 723 | Action: LDelete, 724 | Record: Record{ 725 | Key: key, 726 | }, 727 | }) 728 | 729 | // add to or update writeSet (index of logs) 730 | txn.writeSet[key] = len(txn.logs) - 1 731 | 732 | return nil 733 | } 734 | 735 | func (txn *Txn) Commit() error { 736 | // clearnup readSet before save WAL (S2PL) 737 | for key := range txn.readSet { 738 | txn.s.lock.RUnlock(key) 739 | delete(txn.readSet, key) 740 | } 741 | 742 | err := txn.s.SaveWAL(txn.logs) 743 | if err != nil { 744 | return err 745 | } 746 | 747 | // write back writeSet to db (in memory) 748 | txn.s.ApplyLogs(txn.logs) 749 | 750 | // cleanup writeSet 751 | for key := range txn.writeSet { 752 | txn.s.lock.Unlock(key) 753 | delete(txn.writeSet, key) 754 | } 755 | 756 | // clear logs 757 | // TODO: clear all key and value pointer and reuse logs memory 758 | txn.logs = nil 759 | 760 | return nil 761 | } 762 | 763 | func (txn *Txn) Abort() { 764 | for key := range txn.readSet { 765 | txn.s.lock.RUnlock(key) 766 | delete(txn.readSet, key) 767 | } 768 | for key := range txn.writeSet { 769 | txn.s.lock.Unlock(key) 770 | delete(txn.writeSet, key) 771 | } 772 | txn.logs = nil 773 | } 774 | 775 | func HandleTxn(r io.Reader, w io.WriteCloser, txn *Txn, storage *Storage, closeOnExit bool, wg *sync.WaitGroup) error { 776 | if closeOnExit { 777 | defer w.Close() 778 | defer wg.Done() 779 | } 780 | reader := bufio.NewReader(r) 781 | for { 782 | fmt.Fprintf(w, ">> ") 783 | txt, err := reader.ReadString('\n') 784 | if err != nil { 785 | fmt.Fprintf(w, "failed to read command : %v\n", err) 786 | return err 787 | } 788 | 789 | txt = strings.TrimSpace(txt) 790 | cmd := strings.Split(txt, " ") 791 | if len(cmd) == 0 || len(cmd[0]) == 0 { 792 | continue 793 | } 794 | switch strings.ToLower(cmd[0]) { 795 | case "insert": 796 | if len(cmd) != 3 { 797 | fmt.Fprintf(w, "invalid command : insert \n") 798 | } else if err = txn.Insert(cmd[1], []byte(cmd[2])); err != nil { 799 | fmt.Fprintf(w, "failed to insert : %v\n", err) 800 | } else { 801 | fmt.Fprintf(w, "success to insert %q\n", cmd[1]) 802 | } 803 | 804 | case "update": 805 | if len(cmd) != 3 { 806 | fmt.Fprintf(w, "invalid command : update \n") 807 | } else if err = txn.Update(cmd[1], []byte(cmd[2])); err != nil { 808 | fmt.Fprintf(w, "failed to update : %v\n", err) 809 | } else { 810 | fmt.Fprintf(w, "success to update %q\n", cmd[1]) 811 | } 812 | 813 | case "delete": 814 | if len(cmd) != 2 { 815 | fmt.Fprintf(w, "invalid command : delete \n") 816 | } else if err = txn.Delete(cmd[1]); err != nil { 817 | fmt.Fprintf(w, "failed to delete : %v\n", err) 818 | } else { 819 | fmt.Fprintf(w, "success to delete %q\n", cmd[1]) 820 | } 821 | 822 | case "read": 823 | if len(cmd) != 2 { 824 | fmt.Fprintf(w, "invalid command : read \n") 825 | } else if v, err := txn.Read(cmd[1]); err != nil { 826 | fmt.Fprintf(w, "failed to read : %v\n", err) 827 | } else { 828 | fmt.Fprintf(w, "%v\n", string(v)) 829 | } 830 | 831 | case "commit": 832 | if len(cmd) != 1 { 833 | fmt.Fprintf(w, "invalid command : commit\n") 834 | } else if err = txn.Commit(); err != nil { 835 | fmt.Fprintf(w, "failed to commit : %v\n", err) 836 | } else { 837 | fmt.Fprintf(w, "committed\n") 838 | } 839 | 840 | case "abort": 841 | if len(cmd) != 1 { 842 | fmt.Fprintf(w, "invalid command : abort\n") 843 | } else { 844 | txn.Abort() 845 | fmt.Fprintf(w, "aborted\n") 846 | } 847 | 848 | case "keys": 849 | if len(cmd) != 1 { 850 | fmt.Fprintf(w, "invalid command : keys\n") 851 | } else { 852 | fmt.Fprintf(w, ">>> show keys commited <<<\n") 853 | for k, _ := range storage.db { 854 | fmt.Fprintf(w, "%s\n", k) 855 | } 856 | } 857 | 858 | case "quit", "exit", "q": 859 | fmt.Fprintf(w, "byebye\n") 860 | txn.Abort() 861 | return nil 862 | 863 | default: 864 | fmt.Fprintf(w, "invalid command : not supported\n") 865 | } 866 | } 867 | } 868 | 869 | func main() { 870 | walPath := flag.String("wal", "./txngo.log", "file path of WAL file") 871 | dbPath := flag.String("db", "./txngo.db", "file path of data file") 872 | isInit := flag.Bool("init", true, "create data file if not exist") 873 | tcpaddr := flag.String("tcp", "", "tcp handler address (e.g. localhost:3000)") 874 | 875 | flag.Parse() 876 | 877 | wal, err := os.OpenFile(*walPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) 878 | if err != nil { 879 | log.Panic(err) 880 | } 881 | defer wal.Close() 882 | 883 | storage := NewStorage(wal, *dbPath, *dbPath+".tmp") 884 | 885 | log.Println("loading data file...") 886 | if err = storage.LoadCheckPoint(); os.IsNotExist(err) && *isInit { 887 | log.Println("db file is not found. this is initial start.") 888 | } else if err != nil { 889 | log.Printf("failed to load data file : %v\n", err) 890 | return 891 | } 892 | 893 | log.Println("loading WAL file...") 894 | if nlogs, err := storage.LoadWAL(); err != nil { 895 | log.Printf("failed to load WAL file : %v\n", err) 896 | return 897 | } else if nlogs != 0 { 898 | log.Println("previous shutdown is not success...") 899 | log.Println("update data file...") 900 | if err = storage.SaveCheckPoint(); err != nil { 901 | log.Printf("failed to save checkpoint %v\n", err) 902 | return 903 | } 904 | log.Println("clear WAL file...") 905 | if err = storage.ClearWAL(); err != nil { 906 | log.Printf("failed to clear WAL file %v\n", err) 907 | return 908 | } 909 | } 910 | 911 | log.Println("start transactions") 912 | 913 | if *tcpaddr == "" { 914 | // stdio handler 915 | txn := storage.NewTxn() 916 | err = HandleTxn(os.Stdin, os.Stdout, txn, storage, false, nil) 917 | if err != nil { 918 | log.Println("failed to handle", err) 919 | } 920 | log.Println("shutdown...") 921 | } else { 922 | // tcp handler 923 | l, err := net.Listen("tcp", *tcpaddr) 924 | if err != nil { 925 | log.Println("failed to listen tcp :", err) 926 | return 927 | } 928 | var wg sync.WaitGroup 929 | wg.Add(1) 930 | go func() { 931 | defer wg.Done() 932 | for { 933 | conn, err := l.Accept() 934 | if err != nil { 935 | log.Println("failed to accept tcp :", err) 936 | break 937 | } 938 | log.Println("accept new conn :", conn.RemoteAddr()) 939 | txn := storage.NewTxn() 940 | wg.Add(1) 941 | go HandleTxn(conn, conn, txn, storage, true, &wg) 942 | } 943 | }() 944 | 945 | signal.Reset() 946 | chsig := make(chan os.Signal) 947 | signal.Notify(chsig, os.Interrupt) 948 | <-chsig 949 | log.Println("shutdown...") 950 | l.Close() 951 | 952 | chDone := make(chan struct{}) 953 | go func() { 954 | wg.Wait() 955 | chDone <- struct{}{} 956 | }() 957 | select { 958 | case <-time.After(30 * time.Second): 959 | log.Println("connection not quit. shutdown forcibly.") 960 | return 961 | case <-chDone: 962 | } 963 | } 964 | 965 | log.Println("save checkpoint") 966 | if err = storage.SaveCheckPoint(); err != nil { 967 | log.Printf("failed to save data file : %v\n", err) 968 | } else if err = storage.ClearWAL(); err != nil { 969 | log.Printf("failed to clear WAL file : %v\n", err) 970 | } else { 971 | log.Println("success to save data") 972 | } 973 | } 974 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | const ( 14 | tmpdir = "tmp" 15 | ) 16 | 17 | var ( 18 | testWALPath = filepath.Join(tmpdir, "test.log") 19 | testDBPath = filepath.Join(tmpdir, "test.db") 20 | testTmpPath = filepath.Join(tmpdir, "test.tmp") 21 | ) 22 | 23 | func createTestStorage(t *testing.T) *Storage { 24 | _ = os.RemoveAll(tmpdir) 25 | _ = os.MkdirAll(tmpdir, 0777) 26 | file, err := os.OpenFile(testWALPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | return NewStorage(file, testDBPath, testTmpPath) 31 | } 32 | 33 | func TestTxn_Insert(t *testing.T) { 34 | var ( 35 | value1 = []byte("value1") 36 | value2 = []byte("value2") 37 | value3 = []byte("value3") 38 | ) 39 | t.Run("normal case", func(t *testing.T) { 40 | storage := createTestStorage(t) 41 | defer storage.wal.Close() 42 | txn := storage.NewTxn() 43 | if err := txn.Insert("key1", value1); err != nil { 44 | t.Errorf("failed to insert key1 : %v", err) 45 | } 46 | if err := txn.Insert("key1", value1); err != ErrExist { 47 | t.Errorf("unexpectedly success to insert duplicate key : %v", err) 48 | } 49 | if err := txn.Insert("key2", value2); err != nil { 50 | t.Errorf("failed to insert key2 : %v", err) 51 | } 52 | if err := txn.Commit(); err != nil { 53 | t.Errorf("failed to commit : %v", err) 54 | } 55 | 56 | // insert after commit 57 | if err := txn.Insert("key1", value3); err != ErrExist { 58 | t.Errorf("unexpectedly success to insert duplicate key after commit : %v", err) 59 | } 60 | }) 61 | 62 | t.Run("insert after delete", func(t *testing.T) { 63 | storage := createTestStorage(t) 64 | defer storage.wal.Close() 65 | txn := storage.NewTxn() 66 | if err := txn.Insert("key1", value1); err != nil { 67 | t.Errorf("failed to insert key1 : %v", err) 68 | } 69 | if err := txn.Delete("key1"); err != nil { 70 | t.Errorf("failed to delete key1 : %v", err) 71 | } 72 | if err := txn.Insert("key1", value2); err != nil { 73 | t.Errorf("failed to insert key1 after delete : %v", err) 74 | } 75 | if err := txn.Commit(); err != nil { 76 | t.Errorf("failed to commit : %v", err) 77 | } 78 | if err := txn.Delete("key1"); err != nil { 79 | t.Errorf("failed to delete key1 after commit : %v", err) 80 | } 81 | if err := txn.Insert("key1", value3); err != nil { 82 | t.Errorf("failed to insert key1 after commit and delete : %v", err) 83 | } 84 | }) 85 | } 86 | 87 | func TestTxn_Read(t *testing.T) { 88 | storage := createTestStorage(t) 89 | defer storage.wal.Close() 90 | txn := storage.NewTxn() 91 | value1 := []byte("value1") 92 | if _, err := txn.Read("key1"); err != ErrNotExist { 93 | t.Errorf("key1 is not (not exist) : %v", err) 94 | } 95 | if err := txn.Insert("key1", value1); err != nil { 96 | t.Errorf("failed to insert key1 : %v", err) 97 | } 98 | if v, err := txn.Read("key1"); err != nil { 99 | t.Errorf("failed to read key1 : %v", err) 100 | } else if !bytes.Equal(v, value1) { 101 | t.Errorf("value is not match %v : %v", v, value1) 102 | } 103 | } 104 | 105 | func TestTxn_Update(t *testing.T) { 106 | storage := createTestStorage(t) 107 | defer storage.wal.Close() 108 | txn := storage.NewTxn() 109 | var ( 110 | value1 = []byte("value1") 111 | value2 = []byte("value2") 112 | value3 = []byte("value3") 113 | ) 114 | if err := txn.Update("key1", value1); err != ErrNotExist { 115 | t.Errorf("key1 is not (not exist) : %v", err) 116 | } 117 | if err := txn.Insert("key1", value1); err != nil { 118 | t.Errorf("failed to insert key1 : %v", err) 119 | } 120 | if err := txn.Update("key1", value2); err != nil { 121 | t.Errorf("failed to update key1 : %v", err) 122 | } 123 | if v, err := txn.Read("key1"); err != nil { 124 | t.Errorf("failed to read key1 : %v", err) 125 | } else if !bytes.Equal(v, value2) { 126 | t.Errorf("value is not match %v : %v", v, value2) 127 | } 128 | if err := txn.Commit(); err != nil { 129 | t.Errorf("failed to commit : %v", err) 130 | } 131 | 132 | // update after commit 133 | if err := txn.Update("key1", value3); err != nil { 134 | t.Errorf("failed to update key1 : %v", err) 135 | } 136 | if v, err := txn.Read("key1"); err != nil { 137 | t.Errorf("failed to read key1 after commit : %v", err) 138 | } else if !bytes.Equal(v, value3) { 139 | t.Errorf("value is not match after commit %v : %v", v, value3) 140 | } 141 | } 142 | 143 | func TestTxn_Delete(t *testing.T) { 144 | var ( 145 | value1 = []byte("value1") 146 | ) 147 | t.Run("normal case", func(t *testing.T) { 148 | storage := createTestStorage(t) 149 | defer storage.wal.Close() 150 | txn := storage.NewTxn() 151 | if err := txn.Delete("key1"); err != ErrNotExist { 152 | t.Errorf("key1 is not (not exist) : %v", err) 153 | } 154 | if err := txn.Insert("key1", value1); err != nil { 155 | t.Errorf("failed to insert key1 : %v", err) 156 | } 157 | if err := txn.Delete("key1"); err != nil { 158 | t.Errorf("failed to delete key1 : %v", err) 159 | } 160 | if _, err := txn.Read("key1"); err != ErrNotExist { 161 | t.Errorf("key1 is not (not exist) after delete : %v", err) 162 | } 163 | if err := txn.Delete("key1"); err != ErrNotExist { 164 | t.Errorf("deleted key1 must not exist : %v", err) 165 | } 166 | }) 167 | 168 | t.Run("delete after commit", func(t *testing.T) { 169 | storage := createTestStorage(t) 170 | defer storage.wal.Close() 171 | txn := storage.NewTxn() 172 | if err := txn.Insert("key1", value1); err != nil { 173 | t.Errorf("failed to insert key1 : %v", err) 174 | } 175 | if err := txn.Commit(); err != nil { 176 | t.Errorf("failed to commit : %v", err) 177 | } 178 | 179 | // delete after commit 180 | if err := txn.Delete("key1"); err != nil { 181 | t.Errorf("failed to delete key1 : %v", err) 182 | } 183 | if _, err := txn.Read("key1"); err != ErrNotExist { 184 | t.Errorf("key1 is not (not exist) after delete : %v", err) 185 | } 186 | }) 187 | } 188 | 189 | func TestTxn_Commit(t *testing.T) { 190 | storage := createTestStorage(t) 191 | defer storage.wal.Close() 192 | txn := storage.NewTxn() 193 | var ( 194 | value1 = []byte("value1") 195 | value2 = []byte("value2") 196 | value3 = []byte("value3") 197 | ) 198 | // just insert 199 | if err := txn.Insert("key1", value1); err != nil { 200 | t.Errorf("failed to insert key1 : %v", err) 201 | } 202 | 203 | // updated key 204 | if err := txn.Insert("key2", value2); err != nil { 205 | t.Errorf("failed to insert key2 : %v", err) 206 | } 207 | if err := txn.Update("key2", value3); err != nil { 208 | t.Errorf("failed to update key2 : %v", err) 209 | } 210 | 211 | // deleted key 212 | if err := txn.Insert("key3", value3); err != nil { 213 | t.Errorf("failed to insert key3 : %v", err) 214 | } 215 | if err := txn.Delete("key3"); err != nil { 216 | t.Errorf("failed to delete key3 : %v", err) 217 | } 218 | 219 | // commit 220 | if err := txn.Commit(); err != nil { 221 | t.Errorf("failed to commit") 222 | } 223 | if len(txn.writeSet) != 0 { 224 | t.Errorf("writeSet is not cleared after commit : len == %v", len(txn.writeSet)) 225 | } 226 | if v, err := txn.Read("key1"); err != nil { 227 | t.Errorf("failed to read key1 : %v", err) 228 | } else if !bytes.Equal(v, value1) { 229 | t.Errorf("value1 is not match %v : %v", v, value1) 230 | } 231 | if v, err := txn.Read("key2"); err != nil { 232 | t.Errorf("failed to read key2 : %v", err) 233 | } else if !bytes.Equal(v, value3) { 234 | t.Errorf("value2 is not match %v : %v", v, value3) 235 | } 236 | if _, err := txn.Read("key3"); err != ErrNotExist { 237 | t.Errorf("key3 is not (not exist) : %v", err) 238 | } 239 | } 240 | 241 | func TestTxn_Abort(t *testing.T) { 242 | var ( 243 | value1 = []byte("value1") 244 | value2 = []byte("value2") 245 | ) 246 | t.Run("abort insert", func(t *testing.T) { 247 | storage := createTestStorage(t) 248 | defer storage.wal.Close() 249 | txn := storage.NewTxn() 250 | if err := txn.Insert("key1", value1); err != nil { 251 | t.Errorf("failed to insert key1 : %v", err) 252 | } 253 | txn.Abort() 254 | if _, err := txn.Read("key1"); err != ErrNotExist { 255 | t.Errorf("key1 is not (not exist) : %v", err) 256 | } 257 | }) 258 | 259 | t.Run("abort update", func(t *testing.T) { 260 | storage := createTestStorage(t) 261 | defer storage.wal.Close() 262 | txn := storage.NewTxn() 263 | if err := txn.Insert("key1", value1); err != nil { 264 | t.Errorf("failed to insert key1 : %v", err) 265 | } 266 | if err := txn.Commit(); err != nil { 267 | t.Errorf("failed to commit") 268 | } 269 | 270 | if err := txn.Update("key1", value2); err != nil { 271 | t.Errorf("failed to update key1 : %v", err) 272 | } 273 | txn.Abort() 274 | if v, err := txn.Read("key1"); err != nil { 275 | t.Errorf("failed to read key1 : %v", err) 276 | } else if !bytes.Equal(v, value1) { 277 | t.Errorf("value1 is not match %v : %v", v, value1) 278 | } 279 | }) 280 | 281 | t.Run("abort delete", func(t *testing.T) { 282 | storage := createTestStorage(t) 283 | defer storage.wal.Close() 284 | txn := storage.NewTxn() 285 | if err := txn.Insert("key1", value1); err != nil { 286 | t.Errorf("failed to insert key1 : %v", err) 287 | } 288 | if err := txn.Commit(); err != nil { 289 | t.Errorf("failed to commit") 290 | } 291 | 292 | if err := txn.Delete("key1"); err != nil { 293 | t.Errorf("failed to delete key1 : %v", err) 294 | } 295 | txn.Abort() 296 | if v, err := txn.Read("key1"); err != nil { 297 | t.Errorf("failed to read key1 : %v", err) 298 | } else if !bytes.Equal(v, value1) { 299 | t.Errorf("value1 is not match %v : %v", v, value1) 300 | } 301 | }) 302 | } 303 | 304 | func assertValue(t *testing.T, txn *Txn, key string, value []byte) { 305 | if v, err := txn.Read(key); err != nil { 306 | t.Errorf("failed to read %q : %v", key, err) 307 | } else if !bytes.Equal(v, value) { 308 | t.Errorf("read value for %q is not match %v, expected %v", key, v, value) 309 | } 310 | } 311 | 312 | func assertNotExist(t *testing.T, txn *Txn, key string) { 313 | if v, err := txn.Read(key); err != ErrNotExist { 314 | if err == nil { 315 | t.Errorf("unexpectedly value for %q exists : %v", key, v) 316 | } else { 317 | t.Errorf("failed to read %q expected not exist : %v", key, err) 318 | } 319 | } 320 | } 321 | 322 | func clearFile(t *testing.T, file *os.File) { 323 | if _, err := file.Seek(0, io.SeekStart); err != nil { 324 | t.Errorf("failed to seek : %v", err) 325 | } else if err = file.Truncate(0); err != nil { 326 | t.Errorf("failed to truncate : %v", err) 327 | } 328 | } 329 | 330 | func writeLogs(t *testing.T, file *os.File, logs []RecordLog) { 331 | var buf [4096]byte 332 | for i, rlog := range logs { 333 | if n, err := rlog.Serialize(buf[:]); err != nil { 334 | t.Errorf("failed to deserialize %v : %v", i, err) 335 | } else if _, err = file.Write(buf[:n]); err != nil { 336 | t.Errorf("failed to write log %v : %v", i, err) 337 | } 338 | } 339 | } 340 | 341 | func readLogs(t *testing.T, filename string) ([]byte, []RecordLog) { 342 | buf, err := ioutil.ReadFile(filename) 343 | if err != nil { 344 | t.Errorf("failed to read WAL file : %v", err) 345 | } 346 | var logsInFile []RecordLog 347 | for i := 0; ; i++ { 348 | var rlog RecordLog 349 | n, err := rlog.Deserialize(buf) 350 | if err == ErrBufferShort { 351 | break 352 | } else if err != nil { 353 | t.Fatalf("failed to deserialize log : n == %v : %v : buffer = %v", i, err, buf) 354 | } 355 | logsInFile = append(logsInFile, rlog) 356 | buf = buf[n:] 357 | } 358 | return buf, logsInFile 359 | } 360 | 361 | func applyLogs(t *testing.T, txn *Txn, logs []RecordLog) { 362 | for _, rlog := range logs { 363 | switch rlog.Action { 364 | case LInsert: 365 | if err := txn.Insert(rlog.Key, rlog.Value); err != nil { 366 | t.Errorf("failed to insert %v : %v", rlog, err) 367 | } 368 | 369 | case LUpdate: 370 | if err := txn.Update(rlog.Key, rlog.Value); err != nil { 371 | t.Errorf("failed to update %v : %v", rlog, err) 372 | } 373 | 374 | case LDelete: 375 | if err := txn.Delete(rlog.Key); err != nil { 376 | t.Errorf("failed to delete %v : %v", rlog, err) 377 | } 378 | 379 | case LCommit: 380 | if err := txn.Commit(); err != nil { 381 | t.Errorf("failed to commit %v : %v", rlog, err) 382 | } 383 | 384 | default: 385 | t.Fatalf("unexpected log %v", rlog) 386 | } 387 | } 388 | } 389 | 390 | func TestWAL(t *testing.T) { 391 | storage := createTestStorage(t) 392 | defer storage.wal.Close() 393 | txn := storage.NewTxn() 394 | logs := []RecordLog{ 395 | {Action: LCommit}, 396 | {Action: LInsert, Record: Record{Key: "key1", Value: []byte("value1")}}, 397 | {Action: LInsert, Record: Record{Key: "key2", Value: []byte("value2")}}, 398 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value3")}}, 399 | {Action: LInsert, Record: Record{Key: "key4", Value: []byte("value4")}}, 400 | {Action: LUpdate, Record: Record{Key: "key2", Value: []byte("value5")}}, 401 | {Action: LDelete, Record: Record{Key: "key3", Value: []byte("")}}, // TODO: delete log not need to have value 402 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value6")}}, 403 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value7")}}, 404 | {Action: LCommit}, 405 | {Action: LUpdate, Record: Record{Key: "key1", Value: []byte("value8")}}, 406 | {Action: LDelete, Record: Record{Key: "key2", Value: []byte("")}}, // TODO: delete log not need to have value 407 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value8")}}, 408 | {Action: LCommit}, 409 | } 410 | applyLogs(t, txn, logs) 411 | 412 | rest, logsInFile := readLogs(t, storage.wal.Name()) 413 | if len(rest) != 0 { 414 | t.Fatalf("log file is bigger than expected : %v", rest) 415 | } else if len(logsInFile) != len(logs) { 416 | t.Fatalf("count of log not match %v, expected %v", len(logsInFile), len(logs)) 417 | } 418 | for i := 0; i < len(logs); i++ { 419 | if !reflect.DeepEqual(logsInFile[i], logs[i]) { 420 | t.Errorf("log not match : index == %v, %v, %v", i, logsInFile[i], logs[i]) 421 | } 422 | } 423 | } 424 | 425 | func TestStorage_LoadWAL(t *testing.T) { 426 | t.Run("empty WAL", func(t *testing.T) { 427 | storage := createTestStorage(t) 428 | defer storage.wal.Close() 429 | if n, err := storage.LoadWAL(); err != nil { 430 | t.Errorf("failed to load : %v", err) 431 | } else if n != 0 { 432 | t.Errorf("load wal %v logs, expected 0", n) 433 | } 434 | }) 435 | 436 | t.Run("normal case", func(t *testing.T) { 437 | logs := []RecordLog{ 438 | {Action: LCommit}, 439 | {Action: LInsert, Record: Record{Key: "key1", Value: []byte("value1")}}, 440 | {Action: LInsert, Record: Record{Key: "key2", Value: []byte("value2")}}, 441 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value3")}}, 442 | {Action: LInsert, Record: Record{Key: "key4", Value: []byte("value4")}}, 443 | {Action: LUpdate, Record: Record{Key: "key2", Value: []byte("value5")}}, 444 | {Action: LDelete, Record: Record{Key: "key3", Value: []byte("")}}, // TODO: delete log not need to have value 445 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value6")}}, 446 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value7")}}, 447 | {Action: LCommit}, 448 | {Action: LUpdate, Record: Record{Key: "key1", Value: []byte("value8")}}, 449 | {Action: LDelete, Record: Record{Key: "key2", Value: []byte("")}}, // TODO: delete log not need to have value 450 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value9")}}, 451 | {Action: LCommit}, 452 | } 453 | storage := createTestStorage(t) 454 | defer storage.wal.Close() 455 | txn := storage.NewTxn() 456 | // write log to WAL file 457 | writeLogs(t, storage.wal, logs) 458 | 459 | if n, err := storage.LoadWAL(); err != nil { 460 | t.Errorf("failed to load : %v", err) 461 | } else if n != len(logs) { 462 | t.Errorf("load wal %v logs, expected %v", n, len(logs)) 463 | } 464 | assertValue(t, txn, "key1", []byte("value8")) 465 | assertNotExist(t, txn, "key2") 466 | assertValue(t, txn, "key3", []byte("value9")) 467 | assertValue(t, txn, "key4", []byte("value7")) 468 | 469 | // check idenpotency 470 | clearFile(t, storage.wal) 471 | writeLogs(t, storage.wal, logs) 472 | if n, err := storage.LoadWAL(); err != nil { 473 | t.Errorf("failed to load : %v", err) 474 | } else if n != len(logs) { 475 | t.Errorf("load wal %v logs, expected %v", n, len(logs)) 476 | } 477 | assertValue(t, txn, "key1", []byte("value8")) 478 | assertNotExist(t, txn, "key2") 479 | assertValue(t, txn, "key3", []byte("value9")) 480 | assertValue(t, txn, "key4", []byte("value7")) 481 | }) 482 | 483 | t.Run("log is not completed", func(t *testing.T) { 484 | logs := []RecordLog{ 485 | {Action: LCommit}, 486 | {Action: LInsert, Record: Record{Key: "key1", Value: []byte("value1")}}, 487 | {Action: LInsert, Record: Record{Key: "key2", Value: []byte("value2")}}, 488 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value3")}}, 489 | {Action: LInsert, Record: Record{Key: "key4", Value: []byte("value4")}}, 490 | {Action: LUpdate, Record: Record{Key: "key2", Value: []byte("value5")}}, 491 | {Action: LDelete, Record: Record{Key: "key3", Value: []byte("")}}, // TODO: delete log not need to have value 492 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value6")}}, 493 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value7")}}, 494 | {Action: LCommit}, 495 | {Action: LUpdate, Record: Record{Key: "key1", Value: []byte("value8")}}, 496 | {Action: LDelete, Record: Record{Key: "key2", Value: []byte("")}}, // TODO: delete log not need to have value 497 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value9")}}, 498 | } 499 | storage := createTestStorage(t) 500 | defer storage.wal.Close() 501 | txn := storage.NewTxn() 502 | // write log to WAL file 503 | writeLogs(t, storage.wal, logs) 504 | 505 | if n, err := storage.LoadWAL(); err != nil { 506 | t.Errorf("failed to load : %v", err) 507 | } else if n != len(logs) { 508 | t.Errorf("load wal %v logs, expected %v", n, len(logs)) 509 | } 510 | assertValue(t, txn, "key1", []byte("value1")) 511 | assertValue(t, txn, "key2", []byte("value5")) 512 | assertNotExist(t, txn, "key3") 513 | assertValue(t, txn, "key4", []byte("value7")) 514 | 515 | // check idenpotency 516 | clearFile(t, storage.wal) 517 | writeLogs(t, storage.wal, logs) 518 | if n, err := storage.LoadWAL(); err != nil { 519 | t.Errorf("failed to load : %v", err) 520 | } else if n != len(logs) { 521 | t.Errorf("load wal %v logs, expected %v", n, len(logs)) 522 | } 523 | assertValue(t, txn, "key1", []byte("value1")) 524 | assertValue(t, txn, "key2", []byte("value5")) 525 | assertNotExist(t, txn, "key3") 526 | assertValue(t, txn, "key4", []byte("value7")) 527 | }) 528 | } 529 | 530 | func TestStorage_ClearWAL(t *testing.T) { 531 | logs := []RecordLog{ 532 | {Action: LInsert, Record: Record{Key: "key1", Value: []byte("value1")}}, 533 | {Action: LInsert, Record: Record{Key: "key2", Value: []byte("value2")}}, 534 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value3")}}, 535 | {Action: LCommit}, 536 | } 537 | storage := createTestStorage(t) 538 | defer storage.wal.Close() 539 | writeLogs(t, storage.wal, logs) 540 | 541 | if err := storage.ClearWAL(); err != nil { 542 | t.Errorf("failed to clearWAL : %v", err) 543 | } 544 | 545 | // rewrite from the offset ClearWAL set (must be 0) 546 | writeLogs(t, storage.wal, logs) 547 | 548 | rest, logsInFile := readLogs(t, storage.wal.Name()) 549 | if len(rest) != 0 { 550 | t.Fatalf("log file is bigger than expected : %v", rest) 551 | } else if len(logsInFile) != len(logs) { 552 | t.Fatalf("count of log not match %v, expected %v", len(logsInFile), len(logs)) 553 | } 554 | for i := 0; i < len(logs); i++ { 555 | if !reflect.DeepEqual(logsInFile[i], logs[i]) { 556 | t.Errorf("log not match : index == %v, %v, %v", i, logsInFile[i], logs[i]) 557 | } 558 | } 559 | } 560 | 561 | func TestStorage_SaveCheckPoint(t *testing.T) { 562 | logs := []RecordLog{ 563 | {Action: LCommit}, 564 | {Action: LInsert, Record: Record{Key: "key1", Value: []byte("value1")}}, 565 | {Action: LInsert, Record: Record{Key: "key2", Value: []byte("value2")}}, 566 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value3")}}, 567 | {Action: LInsert, Record: Record{Key: "key4", Value: []byte("value4")}}, 568 | {Action: LUpdate, Record: Record{Key: "key2", Value: []byte("value5")}}, 569 | {Action: LDelete, Record: Record{Key: "key3", Value: []byte("")}}, // TODO: delete log not need to have value 570 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value6")}}, 571 | {Action: LUpdate, Record: Record{Key: "key4", Value: []byte("value7")}}, 572 | {Action: LCommit}, 573 | {Action: LUpdate, Record: Record{Key: "key1", Value: []byte("value8")}}, 574 | {Action: LDelete, Record: Record{Key: "key2", Value: []byte("")}}, // TODO: delete log not need to have value 575 | {Action: LInsert, Record: Record{Key: "key3", Value: []byte("value9")}}, 576 | {Action: LCommit}, 577 | } 578 | storage := createTestStorage(t) 579 | defer storage.wal.Close() 580 | txn := storage.NewTxn() 581 | applyLogs(t, txn, logs) 582 | 583 | if err := storage.SaveCheckPoint(); err != nil { 584 | t.Errorf("failed to save checkpoint : %v", err) 585 | } 586 | 587 | // load checkpoint file by new Txn 588 | wal2, err := os.Open(testWALPath) 589 | if err != nil { 590 | t.Errorf("failed to open wal file : %v", err) 591 | } 592 | storage2 := NewStorage(wal2, testDBPath, testTmpPath) 593 | if err = storage2.LoadCheckPoint(); err != nil { 594 | t.Errorf("failed to load checkpoint : %v", err) 595 | } 596 | if !reflect.DeepEqual(storage2.db, storage2.db) { 597 | t.Errorf("loaded records not match") 598 | } 599 | } 600 | --------------------------------------------------------------------------------