db_service.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // Copyright (C) 2025 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package sqlite
  7. import (
  8. "context"
  9. "fmt"
  10. "time"
  11. "github.com/syncthing/syncthing/internal/db"
  12. )
  13. const (
  14. internalMetaPrefix = "dbsvc"
  15. lastMaintKey = "lastMaint"
  16. defaultDeleteRetention = 180 * 24 * time.Hour
  17. minDeleteRetention = 24 * time.Hour
  18. )
  19. type Service struct {
  20. sdb *DB
  21. maintenanceInterval time.Duration
  22. internalMeta *db.Typed
  23. }
  24. func (s *Service) String() string {
  25. return fmt.Sprintf("sqlite.service@%p", s)
  26. }
  27. func newService(sdb *DB, maintenanceInterval time.Duration) *Service {
  28. return &Service{
  29. sdb: sdb,
  30. maintenanceInterval: maintenanceInterval,
  31. internalMeta: db.NewTyped(sdb, internalMetaPrefix),
  32. }
  33. }
  34. func (s *Service) Serve(ctx context.Context) error {
  35. // Run periodic maintenance
  36. // Figure out when we last ran maintenance and schedule accordingly. If
  37. // it was never, do it now.
  38. lastMaint, _, _ := s.internalMeta.Time(lastMaintKey)
  39. nextMaint := lastMaint.Add(s.maintenanceInterval)
  40. wait := time.Until(nextMaint)
  41. if wait < 0 {
  42. wait = time.Minute
  43. }
  44. l.Debugln("Next periodic run in", wait)
  45. timer := time.NewTimer(wait)
  46. for {
  47. select {
  48. case <-ctx.Done():
  49. return ctx.Err()
  50. case <-timer.C:
  51. }
  52. if err := s.periodic(ctx); err != nil {
  53. return wrap(err)
  54. }
  55. timer.Reset(s.maintenanceInterval)
  56. l.Debugln("Next periodic run in", s.maintenanceInterval)
  57. _ = s.internalMeta.PutTime(lastMaintKey, time.Now())
  58. }
  59. }
  60. func (s *Service) periodic(ctx context.Context) error {
  61. t0 := time.Now()
  62. l.Debugln("Periodic start")
  63. s.sdb.updateLock.Lock()
  64. defer s.sdb.updateLock.Unlock()
  65. t1 := time.Now()
  66. defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
  67. if err := s.garbageCollectOldDeletedLocked(); err != nil {
  68. return wrap(err)
  69. }
  70. if err := s.garbageCollectBlocklistsAndBlocksLocked(ctx); err != nil {
  71. return wrap(err)
  72. }
  73. conn, err := s.sdb.sql.Conn(ctx)
  74. if err != nil {
  75. return wrap(err)
  76. }
  77. defer conn.Close()
  78. _, _ = conn.ExecContext(ctx, `ANALYZE`)
  79. _, _ = conn.ExecContext(ctx, `PRAGMA optimize`)
  80. _, _ = conn.ExecContext(ctx, `PRAGMA incremental_vacuum`)
  81. _, _ = conn.ExecContext(ctx, `PRAGMA journal_size_limit = 67108864`)
  82. _, _ = conn.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
  83. return nil
  84. }
  85. func (s *Service) garbageCollectOldDeletedLocked() error {
  86. if s.sdb.deleteRetention <= 0 {
  87. l.Debugln("Delete retention is infinite, skipping cleanup")
  88. return nil
  89. }
  90. // Remove deleted files that are marked as not needed (we have processed
  91. // them) and they were deleted more than MaxDeletedFileAge ago.
  92. l.Debugln("Forgetting deleted files older than", s.sdb.deleteRetention)
  93. res, err := s.sdb.stmt(`
  94. DELETE FROM files
  95. WHERE deleted AND modified < ? AND local_flags & {{.FlagLocalNeeded}} == 0
  96. `).Exec(time.Now().Add(-s.sdb.deleteRetention).UnixNano())
  97. if err != nil {
  98. return wrap(err)
  99. }
  100. if aff, err := res.RowsAffected(); err == nil {
  101. l.Debugln("Removed old deleted file records:", aff)
  102. }
  103. return nil
  104. }
  105. func (s *Service) garbageCollectBlocklistsAndBlocksLocked(ctx context.Context) error {
  106. // Remove all blocklists not referred to by any files and, by extension,
  107. // any blocks not referred to by a blocklist. This is an expensive
  108. // operation when run normally, especially if there are a lot of blocks
  109. // to collect.
  110. //
  111. // We make this orders of magnitude faster by disabling foreign keys for
  112. // the transaction and doing the cleanup manually. This requires using
  113. // an explicit connection and disabling foreign keys before starting the
  114. // transaction. We make sure to clean up on the way out.
  115. conn, err := s.sdb.sql.Connx(ctx)
  116. if err != nil {
  117. return wrap(err)
  118. }
  119. defer conn.Close()
  120. if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = 0`); err != nil {
  121. return wrap(err)
  122. }
  123. defer func() { //nolint:contextcheck
  124. _, _ = conn.ExecContext(context.Background(), `PRAGMA foreign_keys = 1`)
  125. }()
  126. tx, err := conn.BeginTxx(ctx, nil)
  127. if err != nil {
  128. return wrap(err)
  129. }
  130. defer tx.Rollback() //nolint:errcheck
  131. if res, err := tx.ExecContext(ctx, `
  132. DELETE FROM blocklists
  133. WHERE NOT EXISTS (
  134. SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
  135. )`); err != nil {
  136. return wrap(err, "delete blocklists")
  137. } else if shouldDebug() {
  138. rows, err := res.RowsAffected()
  139. l.Debugln("Blocklist GC:", rows, err)
  140. }
  141. if res, err := tx.ExecContext(ctx, `
  142. DELETE FROM blocks
  143. WHERE NOT EXISTS (
  144. SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
  145. )`); err != nil {
  146. return wrap(err, "delete blocks")
  147. } else if shouldDebug() {
  148. rows, err := res.RowsAffected()
  149. l.Debugln("Blocks GC:", rows, err)
  150. }
  151. return wrap(tx.Commit())
  152. }