1
0

client_sync_orphan.go 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. package service
  2. import (
  3. "time"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  7. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  8. "gorm.io/gorm"
  9. )
  10. // How long a client stays recoverable after a node merge concluded it is gone.
  11. // Any merge that sees it attached again inside this window clears the mark.
  12. const syncOrphanReapGrace = 15 * time.Minute
  13. // A traffic row with no clients row behind it is drift the orphan sweep will
  14. // never mark, so it stays on the old delete-immediately path.
  15. func clientRecordExists(tx *gorm.DB, email string) bool {
  16. var n int64
  17. if err := tx.Model(&model.ClientRecord{}).Where("email = ?", email).Count(&n).Error; err != nil {
  18. return false
  19. }
  20. return n > 0
  21. }
  22. func markSyncOrphan(tx *gorm.DB, email string, nowMs int64) error {
  23. if email == "" {
  24. return nil
  25. }
  26. return tx.Model(&model.ClientRecord{}).
  27. Where("email = ? AND sync_orphaned_at = 0", email).
  28. Update("sync_orphaned_at", nowMs).Error
  29. }
  30. // A client that is attached again is not orphaned, whatever an earlier merge
  31. // concluded — this is what makes a bad merge recoverable instead of fatal.
  32. func clearSyncOrphanMarks(tx *gorm.DB) error {
  33. return tx.Model(&model.ClientRecord{}).
  34. Where("sync_orphaned_at > 0 AND EXISTS (SELECT 1 FROM client_inbounds WHERE client_inbounds.client_id = clients.id)").
  35. Update("sync_orphaned_at", 0).Error
  36. }
  37. // ReapSyncOrphans deletes the clients the node-snapshot sweep marked and no
  38. // later merge reclaimed. It is the only path that hard-deletes for that sweep.
  39. func (s *ClientService) ReapSyncOrphans() (int, error) {
  40. db := database.GetDB()
  41. cutoff := time.Now().Add(-syncOrphanReapGrace).UnixMilli()
  42. var emails []string
  43. if err := db.Model(&model.ClientRecord{}).
  44. Where("sync_orphaned_at > 0 AND sync_orphaned_at <= ?", cutoff).
  45. Where("NOT EXISTS (SELECT 1 FROM client_inbounds WHERE client_inbounds.client_id = clients.id)").
  46. Pluck("email", &emails).Error; err != nil {
  47. return 0, err
  48. }
  49. if len(emails) == 0 {
  50. return 0, nil
  51. }
  52. reaped := 0
  53. for _, batch := range chunkStrings(emails, sqlInChunk) {
  54. if err := runSerializedTx(func(tx *gorm.DB) error {
  55. if err := adjustGroupBaselinesForRemovedTraffic(tx, batch); err != nil {
  56. return err
  57. }
  58. if err := tx.Where("email IN ?", batch).Delete(&model.ClientRecord{}).Error; err != nil {
  59. return err
  60. }
  61. if err := tx.Where("email IN ?", batch).Delete(&xray.ClientTraffic{}).Error; err != nil {
  62. return err
  63. }
  64. if err := tx.Where("email IN ?", batch).Delete(&model.NodeClientTraffic{}).Error; err != nil {
  65. return err
  66. }
  67. return tx.Where("client_email IN ?", batch).Delete(&model.InboundClientIps{}).Error
  68. }); err != nil {
  69. return reaped, err
  70. }
  71. reaped += len(batch)
  72. logger.Infof("reaped %d client(s) confirmed removed on their node", len(batch))
  73. }
  74. return reaped, nil
  75. }