db_service.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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. "time"
  10. "github.com/syncthing/syncthing/internal/db"
  11. )
  12. const (
  13. internalMetaPrefix = "dbsvc"
  14. lastMaintKey = "lastMaint"
  15. )
  16. type Service struct {
  17. sdb *DB
  18. maintenanceInterval time.Duration
  19. internalMeta *db.Typed
  20. }
  21. func newService(sdb *DB, maintenanceInterval time.Duration) *Service {
  22. return &Service{
  23. sdb: sdb,
  24. maintenanceInterval: maintenanceInterval,
  25. internalMeta: db.NewTyped(sdb, internalMetaPrefix),
  26. }
  27. }
  28. func (s *Service) Serve(ctx context.Context) error {
  29. // Run periodic maintenance
  30. // Figure out when we last ran maintenance and schedule accordingly. If
  31. // it was never, do it now.
  32. lastMaint, _, _ := s.internalMeta.Time(lastMaintKey)
  33. nextMaint := lastMaint.Add(s.maintenanceInterval)
  34. wait := time.Until(nextMaint)
  35. if wait < 0 {
  36. wait = time.Minute
  37. }
  38. l.Debugln("Next periodic run in", wait)
  39. timer := time.NewTimer(wait)
  40. for {
  41. select {
  42. case <-ctx.Done():
  43. return ctx.Err()
  44. case <-timer.C:
  45. }
  46. if err := s.periodic(ctx); err != nil {
  47. return wrap(err)
  48. }
  49. timer.Reset(s.maintenanceInterval)
  50. l.Debugln("Next periodic run in", s.maintenanceInterval)
  51. _ = s.internalMeta.PutTime(lastMaintKey, time.Now())
  52. }
  53. }
  54. func (s *Service) periodic(ctx context.Context) error {
  55. t0 := time.Now()
  56. l.Debugln("Periodic start")
  57. s.sdb.updateLock.Lock()
  58. defer s.sdb.updateLock.Unlock()
  59. t1 := time.Now()
  60. defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
  61. if err := s.garbageCollectBlocklistsAndBlocksLocked(ctx); err != nil {
  62. return wrap(err)
  63. }
  64. _, _ = s.sdb.sql.ExecContext(ctx, `ANALYZE`)
  65. _, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA optimize`)
  66. _, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA incremental_vacuum`)
  67. _, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
  68. return nil
  69. }
  70. func (s *Service) garbageCollectBlocklistsAndBlocksLocked(ctx context.Context) error {
  71. // Remove all blocklists not referred to by any files and, by extension,
  72. // any blocks not referred to by a blocklist. This is an expensive
  73. // operation when run normally, especially if there are a lot of blocks
  74. // to collect.
  75. //
  76. // We make this orders of magnitude faster by disabling foreign keys for
  77. // the transaction and doing the cleanup manually. This requires using
  78. // an explicit connection and disabling foreign keys before starting the
  79. // transaction. We make sure to clean up on the way out.
  80. conn, err := s.sdb.sql.Connx(ctx)
  81. if err != nil {
  82. return wrap(err)
  83. }
  84. defer conn.Close()
  85. if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = 0`); err != nil {
  86. return wrap(err)
  87. }
  88. defer func() { //nolint:contextcheck
  89. _, _ = conn.ExecContext(context.Background(), `PRAGMA foreign_keys = 1`)
  90. }()
  91. tx, err := conn.BeginTxx(ctx, nil)
  92. if err != nil {
  93. return wrap(err)
  94. }
  95. defer tx.Rollback() //nolint:errcheck
  96. if res, err := tx.ExecContext(ctx, `
  97. DELETE FROM blocklists
  98. WHERE NOT EXISTS (
  99. SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
  100. )`); err != nil {
  101. return wrap(err, "delete blocklists")
  102. } else if shouldDebug() {
  103. rows, err := res.RowsAffected()
  104. l.Debugln("Blocklist GC:", rows, err)
  105. }
  106. if res, err := tx.ExecContext(ctx, `
  107. DELETE FROM blocks
  108. WHERE NOT EXISTS (
  109. SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
  110. )`); err != nil {
  111. return wrap(err, "delete blocks")
  112. } else if shouldDebug() {
  113. rows, err := res.RowsAffected()
  114. l.Debugln("Blocks GC:", rows, err)
  115. }
  116. return wrap(tx.Commit())
  117. }