1515package backend
1616
1717import (
18+ "errors"
1819 "fmt"
1920 "hash/crc32"
2021 "io"
@@ -28,6 +29,7 @@ import (
2829 "go.uber.org/zap"
2930
3031 bolt "go.etcd.io/bbolt"
32+ bolterrors "go.etcd.io/bbolt/errors"
3133 "go.etcd.io/etcd/client/pkg/v3/verify"
3234)
3335
@@ -469,20 +471,6 @@ func (b *backend) defrag() error {
469471 isDefragActive .Set (1 )
470472 defer isDefragActive .Set (0 )
471473
472- // TODO: make this non-blocking?
473- // lock batchTx to ensure nobody is using previous tx, and then
474- // close previous ongoing tx.
475- b .batchTx .LockOutsideApply ()
476- defer b .batchTx .Unlock ()
477-
478- // lock database after lock tx to avoid deadlock.
479- b .mu .Lock ()
480- defer b .mu .Unlock ()
481-
482- // block concurrent read requests while resetting tx
483- b .readTx .Lock ()
484- defer b .readTx .Unlock ()
485-
486474 // Create a temporary file to ensure we start with a clean slate.
487475 // Snapshotter.cleanupSnapdir cleans up any of these that are found during startup.
488476 dir := filepath .Dir (b .db .Path ())
@@ -500,63 +488,112 @@ func (b *backend) defrag() error {
500488 // return nil, fmt.Errorf(defragOpenFileError)
501489 return temp , nil
502490 }
503- // Don't load tmp db into memory regardless of opening options
504491 options .Mlock = false
505492 tdbp := temp .Name ()
506493 tmpdb , err := bolt .Open (tdbp , 0o600 , & options )
507494 if err != nil {
508495 temp .Close ()
509496 if rmErr := os .Remove (temp .Name ()); rmErr != nil {
510- b .lg .Error (
511- "failed to remove temporary file" ,
497+ b .lg .Error ("failed to remove temporary file" ,
512498 zap .String ("path" , temp .Name ()),
513499 zap .Error (rmErr ),
514500 )
515501 }
516-
517502 return err
518503 }
519504
520505 dbp := b .db .Path ()
521506 size1 , sizeInUse1 := b .Size (), b .SizeInUse ()
522- b .lg .Info (
523- "defragmenting" ,
507+ b .lg .Info ("defragmenting" ,
524508 zap .String ("path" , dbp ),
525509 zap .Int64 ("current-db-size-bytes" , size1 ),
526510 zap .String ("current-db-size" , humanize .Bytes (uint64 (size1 ))),
527511 zap .Int64 ("current-db-size-in-use-bytes" , sizeInUse1 ),
528512 zap .String ("current-db-size-in-use" , humanize .Bytes (uint64 (sizeInUse1 ))),
529513 )
530514
531- defer func () {
532- // NOTE: We should exit as soon as possible because that tx
533- // might be closed. The inflight request might use invalid
534- // tx and then panic as well. The real panic reason might be
535- // shadowed by new panic. So, we should fatal here with lock.
536- if rerr := recover (); rerr != nil {
537- b .lg .Fatal ("unexpected panic during defrag" , zap .Any ("panic" , rerr ))
538- }
539- }()
515+ // ============================================================
516+ // PHASE 1: Commit pending writes, take a snapshot, install the
517+ // journal, then unlock so writes can continue during the copy.
518+ // ============================================================
519+ b .batchTx .LockOutsideApply ()
540520
541- // Commit/stop and then reset current transactions (including the readTx)
542- b .batchTx .unsafeCommit (true )
543- b .batchTx .tx = nil
521+ b .batchTx .commit (false )
522+
523+ b .mu .RLock ()
524+ snapTx , snapErr := b .db .Begin (false )
525+ b .mu .RUnlock ()
526+ if snapErr != nil {
527+ b .batchTx .Unlock ()
528+ tmpdb .Close ()
529+ os .Remove (tdbp )
530+ return fmt .Errorf ("failed to begin snapshot tx for defrag: %w" , snapErr )
531+ }
532+
533+ journal := newDefragJournal ()
534+ b .batchTx .defragJournal = journal
535+ b .batchTx .Unlock ()
536+
537+ b .lg .Info ("defrag: copying data (writes unlocked)" )
544538
545539 // gofail: var defragBeforeCopy struct{}
546- err = defragdb (b .db , tmpdb , defragLimit )
540+ err = defragFromTx (snapTx , tmpdb , defragLimit )
541+ snapTx .Rollback ()
542+
547543 if err != nil {
544+ b .batchTx .LockOutsideApply ()
545+ b .batchTx .defragJournal = nil
546+ b .batchTx .Unlock ()
547+ journal .close ()
548548 tmpdb .Close ()
549- if rmErr := os .RemoveAll (tmpdb .Path ()); rmErr != nil {
550- b .lg .Error ("failed to remove db.tmp after defragmentation completed" , zap .Error (rmErr ))
551- }
549+ os .RemoveAll (tdbp )
550+ return err
551+ }
552+
553+ // ============================================================
554+ // PHASE 2: Lock writes, drain and replay the journal into the
555+ // temp DB. This should be fast since it's only the delta.
556+ // ============================================================
557+ b .lg .Info ("defrag: replaying journal" )
552558
553- // restore the bbolt transactions if defragmentation fails
554- b .batchTx .tx = b .unsafeBegin (true )
555- b .readTx .tx = b .unsafeBegin (false )
559+ b .batchTx .LockOutsideApply ()
560+ b .batchTx .defragJournal = nil
561+ journal .close ()
562+ ops := journal .drain ()
563+
564+ if len (ops ) > 0 {
565+ b .lg .Info ("defrag: replaying journal ops" , zap .Int ("count" , len (ops )))
566+ }
556567
568+ err = replayJournal (tmpdb , ops , defragLimit )
569+ if err != nil {
570+ b .batchTx .Unlock ()
571+ tmpdb .Close ()
572+ os .RemoveAll (tdbp )
557573 return err
558574 }
559575
576+ // ============================================================
577+ // PHASE 3: Atomic switchover — close old db, rename, reopen.
578+ // ============================================================
579+ b .lg .Info ("defrag: switching database" )
580+
581+ b .mu .Lock ()
582+ b .readTx .Lock ()
583+
584+ // NOTE: We should exit as soon as possible because that tx
585+ // might be closed. The inflight request might use invalid
586+ // tx and then panic as well. The real panic reason might be
587+ // shadowed by new panic. So, we should fatal here with lock.
588+ defer func () {
589+ if rerr := recover (); rerr != nil {
590+ b .lg .Fatal ("unexpected panic during defrag" , zap .Any ("panic" , rerr ))
591+ }
592+ }()
593+
594+ b .batchTx .unsafeCommit (true )
595+ b .batchTx .tx = nil
596+
560597 err = b .db .Close ()
561598 if err != nil {
562599 b .lg .Fatal ("failed to close database" , zap .Error (err ))
@@ -585,12 +622,15 @@ func (b *backend) defrag() error {
585622 atomic .StoreInt64 (& b .size , size )
586623 atomic .StoreInt64 (& b .sizeInUse , size - (int64 (db .Stats ().FreePageN )* int64 (db .Info ().PageSize )))
587624
625+ b .readTx .Unlock ()
626+ b .mu .Unlock ()
627+ b .batchTx .Unlock ()
628+
588629 took := time .Since (now )
589630 defragSec .Observe (took .Seconds ())
590631
591632 size2 , sizeInUse2 := b .Size (), b .SizeInUse ()
592- b .lg .Info (
593- "finished defragmenting directory" ,
633+ b .lg .Info ("finished defragmenting directory" ,
594634 zap .String ("path" , dbp ),
595635 zap .Int64 ("current-db-size-bytes-diff" , size2 - size1 ),
596636 zap .Int64 ("current-db-size-bytes" , size2 ),
@@ -603,11 +643,7 @@ func (b *backend) defrag() error {
603643 return nil
604644}
605645
606- func defragdb (odb , tmpdb * bolt.DB , limit int ) error {
607- // gofail: var defragdbFail string
608- // return fmt.Errorf(defragdbFail)
609-
610- // open a tx on tmpdb for writes
646+ func defragFromTx (srcTx * bolt.Tx , tmpdb * bolt.DB , limit int ) error {
611647 tmptx , err := tmpdb .Begin (true )
612648 if err != nil {
613649 return err
@@ -618,18 +654,10 @@ func defragdb(odb, tmpdb *bolt.DB, limit int) error {
618654 }
619655 }()
620656
621- // open a tx on old db for read
622- tx , err := odb .Begin (false )
623- if err != nil {
624- return err
625- }
626- defer tx .Rollback ()
627-
628- c := tx .Cursor ()
629-
657+ c := srcTx .Cursor ()
630658 count := 0
631659 for next , _ := c .First (); next != nil ; next , _ = c .Next () {
632- b := tx .Bucket (next )
660+ b := srcTx .Bucket (next )
633661 if b == nil {
634662 return fmt .Errorf ("backend: cannot defrag bucket %s" , next )
635663 }
@@ -665,6 +693,69 @@ func defragdb(odb, tmpdb *bolt.DB, limit int) error {
665693 return tmptx .Commit ()
666694}
667695
696+ func replayJournal (tmpdb * bolt.DB , ops []defragJournalOp , limit int ) error {
697+ if len (ops ) == 0 {
698+ return nil
699+ }
700+
701+ tx , err := tmpdb .Begin (true )
702+ if err != nil {
703+ return err
704+ }
705+ defer func () {
706+ if err != nil {
707+ tx .Rollback ()
708+ }
709+ }()
710+
711+ count := 0
712+ for _ , op := range ops {
713+ count ++
714+ if count > limit {
715+ if err = tx .Commit (); err != nil {
716+ return err
717+ }
718+ tx , err = tmpdb .Begin (true )
719+ if err != nil {
720+ return err
721+ }
722+ count = 0
723+ }
724+
725+ switch op .opType {
726+ case opCreateBucket :
727+ if _ , err = tx .CreateBucketIfNotExists (op .bucketName ); err != nil {
728+ return fmt .Errorf ("replay: create bucket %s: %w" , op .bucketName , err )
729+ }
730+ case opDeleteBucket :
731+ if delErr := tx .DeleteBucket (op .bucketName ); delErr != nil && ! errors .Is (delErr , bolterrors .ErrBucketNotFound ) {
732+ return fmt .Errorf ("replay: delete bucket %s: %w" , op .bucketName , delErr )
733+ }
734+ case opPut :
735+ b := tx .Bucket (op .bucketName )
736+ if b == nil {
737+ return fmt .Errorf ("replay: bucket %s not found for put" , op .bucketName )
738+ }
739+ if op .seq {
740+ b .FillPercent = 0.9
741+ }
742+ if err = b .Put (op .key , op .value ); err != nil {
743+ return fmt .Errorf ("replay: put in bucket %s: %w" , op .bucketName , err )
744+ }
745+ case opDelete :
746+ b := tx .Bucket (op .bucketName )
747+ if b == nil {
748+ continue
749+ }
750+ if err = b .Delete (op .key ); err != nil {
751+ return fmt .Errorf ("replay: delete from bucket %s: %w" , op .bucketName , err )
752+ }
753+ }
754+ }
755+
756+ return tx .Commit ()
757+ }
758+
668759func (b * backend ) begin (write bool ) * bolt.Tx {
669760 b .mu .RLock ()
670761 tx := b .unsafeBegin (write )
0 commit comments