dataDownloadFileRequestService.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sync"
  7. "time"
  8. "fwh-toolbox-server/cache"
  9. "fwh-toolbox-server/domain"
  10. mongoRepo "fwh-toolbox-server/repositories/mongo"
  11. "github.com/redis/go-redis/v9"
  12. "go.mongodb.org/mongo-driver/mongo"
  13. "go.uber.org/zap"
  14. )
  15. const (
  16. dataDownloadRequestLeaseTTL = 2 * time.Minute
  17. dataDownloadCompletedTTL = time.Hour
  18. )
  19. // DataDownloadRequestDeferred tells the queue to retry an item later without
  20. // occupying a worker while another request for the same document is running.
  21. type DataDownloadRequestDeferred struct {
  22. RequestID string
  23. After time.Duration
  24. }
  25. func (e *DataDownloadRequestDeferred) Error() string {
  26. return fmt.Sprintf("data download request %s is still running", e.RequestID)
  27. }
  28. type dataDownloadRequestLease struct {
  29. documentID string
  30. requestKey string
  31. requestID string
  32. lock *cache.RedisLock
  33. cancel context.CancelFunc
  34. done chan struct{}
  35. lost chan struct{}
  36. stopOnce sync.Once
  37. }
  38. func dataDownloadRequestKey(documentID string) string {
  39. return "data-download:request:" + documentID
  40. }
  41. func dataDownloadCompletedKey(documentID string) string {
  42. return "data-download:completed:" + documentID
  43. }
  44. func prepareDataDownloadRequest(ctx context.Context, item *domain.DataDownloadItem, awaitRequestID string) (*domain.DataDownloadDocument, *dataDownloadRequestLease, error) {
  45. requestKey, err := buildDataDownloadRequestKey(item)
  46. if err != nil {
  47. return nil, nil, err
  48. }
  49. documentID := stableDataDownloadDocumentID(requestKey)
  50. if existing, err := mongoRepo.GetDataDownloadDocument(ctx, documentID); err == nil && existing.State == domain.DataDownloadFrozen {
  51. return existing, nil, nil
  52. } else if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
  53. return nil, nil, err
  54. }
  55. if awaitRequestID != "" && item.Kind != domain.DataDownloadUpdateLog {
  56. completed, err := cache.Get(ctx, dataDownloadCompletedKey(documentID))
  57. if err == nil && completed == awaitRequestID {
  58. document, getErr := mongoRepo.GetDataDownloadDocument(ctx, documentID)
  59. if getErr == nil {
  60. return document, nil, nil
  61. }
  62. if !errors.Is(getErr, mongo.ErrNoDocuments) {
  63. return nil, nil, getErr
  64. }
  65. }
  66. if err != nil && !errors.Is(err, redis.Nil) {
  67. return nil, nil, err
  68. }
  69. }
  70. lock, ok, err := cache.TryLock(ctx, dataDownloadRequestKey(documentID), dataDownloadRequestLeaseTTL)
  71. if err != nil {
  72. return nil, nil, err
  73. }
  74. if !ok {
  75. requestID, getErr := cache.Get(ctx, dataDownloadRequestKey(documentID))
  76. if getErr != nil && !errors.Is(getErr, redis.Nil) {
  77. return nil, nil, getErr
  78. }
  79. if requestID == "" {
  80. requestID = awaitRequestID
  81. }
  82. zap.L().Debug("[DataDownload] 请求租约被占用,本 item 延迟等待",
  83. zap.String("itemId", item.ID.Hex()),
  84. zap.String("documentId", documentID),
  85. zap.String("kind", string(item.Kind)),
  86. zap.String("sn", item.SiteSN),
  87. zap.String("holderRequestId", requestID),
  88. )
  89. return nil, nil, &DataDownloadRequestDeferred{RequestID: requestID, After: 15 * time.Second}
  90. }
  91. // The document may have become frozen between the first lookup and lease acquisition.
  92. if existing, getErr := mongoRepo.GetDataDownloadDocument(ctx, documentID); getErr == nil && existing.State == domain.DataDownloadFrozen {
  93. lock.SafeRelease(context.Background())
  94. return existing, nil, nil
  95. } else if getErr != nil && !errors.Is(getErr, mongo.ErrNoDocuments) {
  96. lock.SafeRelease(context.Background())
  97. return nil, nil, getErr
  98. }
  99. renewCtx, cancel := context.WithCancel(context.Background())
  100. lease := &dataDownloadRequestLease{
  101. documentID: documentID,
  102. requestKey: requestKey,
  103. requestID: lock.GetToken(),
  104. lock: lock,
  105. cancel: cancel,
  106. done: make(chan struct{}),
  107. lost: make(chan struct{}),
  108. }
  109. go lease.renew(renewCtx)
  110. zap.L().Debug("[DataDownload] 已获取请求租约,进入执行",
  111. zap.String("itemId", item.ID.Hex()),
  112. zap.String("documentId", documentID),
  113. zap.String("kind", string(item.Kind)),
  114. zap.String("sn", item.SiteSN),
  115. zap.String("subtype", item.Subtype),
  116. zap.String("date", item.Date),
  117. )
  118. return nil, lease, nil
  119. }
  120. func (l *dataDownloadRequestLease) renew(ctx context.Context) {
  121. defer close(l.done)
  122. ticker := time.NewTicker(dataDownloadRequestLeaseTTL / 3)
  123. defer ticker.Stop()
  124. for {
  125. select {
  126. case <-ctx.Done():
  127. return
  128. case <-ticker.C:
  129. renewCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  130. err := l.lock.Extend(renewCtx, dataDownloadRequestLeaseTTL)
  131. cancel()
  132. if err != nil {
  133. zap.L().Warn("[DataDownload] 请求租约续期失败,租约丢失",
  134. zap.String("documentId", l.documentID),
  135. zap.String("requestId", l.requestID),
  136. zap.String("err", err.Error()),
  137. )
  138. close(l.lost)
  139. return
  140. }
  141. }
  142. }
  143. }
  144. func (l *dataDownloadRequestLease) validate(ctx context.Context) error {
  145. select {
  146. case <-l.lost:
  147. return fmt.Errorf("data download request lease was lost")
  148. default:
  149. }
  150. ok, err := l.lock.IsOwner(ctx)
  151. if err != nil {
  152. return err
  153. }
  154. if !ok {
  155. return fmt.Errorf("data download request lease was lost")
  156. }
  157. return nil
  158. }
  159. func (l *dataDownloadRequestLease) complete(ctx context.Context) error {
  160. if err := l.lock.SetIfOwner(ctx, dataDownloadCompletedKey(l.documentID), l.requestID, dataDownloadCompletedTTL); err != nil {
  161. l.stop()
  162. return err
  163. }
  164. l.stop()
  165. return nil
  166. }
  167. func (l *dataDownloadRequestLease) abort() {
  168. l.stop()
  169. }
  170. func (l *dataDownloadRequestLease) stop() {
  171. l.stopOnce.Do(func() {
  172. l.cancel()
  173. <-l.done
  174. l.lock.SafeRelease(context.Background())
  175. })
  176. }