dataDownloadFaultRecordCall.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "fwh-toolbox-server/domain"
  10. "fwh-toolbox-server/repositories"
  11. mongoRepo "fwh-toolbox-server/repositories/mongo"
  12. "go.uber.org/zap"
  13. )
  14. const (
  15. faultCallPageSize = 50
  16. faultCallPollInterval = time.Minute
  17. faultCallTimeout = 2 * time.Hour
  18. )
  19. type DataDownloadFaultCallDeferred struct{ After time.Duration }
  20. func (e *DataDownloadFaultCallDeferred) Error() string {
  21. return "fault recording call is still running"
  22. }
  23. var errAmbiguousFaultCall = errors.New("ambiguous fault recording call")
  24. var ErrFaultRecordCallTimeout = errors.New("fault recording call timed out")
  25. var ErrFaultRecordCallFailed = errors.New("fault recording call failed")
  26. func faultCallTimeoutError(item *domain.DataDownloadItem, lastStatus string) error {
  27. return fmt.Errorf("%w after 2h (recordId=%d, lastStatus=%s)", ErrFaultRecordCallTimeout, item.CallRecordID, lastStatus)
  28. }
  29. type faultRecordSource struct {
  30. record domain.FaultRecordInfo
  31. sub domain.FaultRecordSubInfo
  32. }
  33. // resolveFaultRecordSources checks the callable catalog first. Only a subrecord
  34. // whose failure time and fault code match a catalog entry proves that an
  35. // existing remote record can be reused.
  36. func resolveFaultRecordSources(ctx context.Context, item *domain.DataDownloadItem, documentID string, deviceType int, start, end time.Time) ([]faultRecordSource, error) {
  37. callable, err := repositories.QueryFaultRecordCallableList(deviceType, item.DeviceSN)
  38. if err != nil {
  39. return nil, fmt.Errorf("query callable fault records: %w", err)
  40. }
  41. selected := make([]domain.FaultRecordCallableInfo, 0)
  42. for _, candidate := range callable {
  43. failureTime, parseErr := time.ParseInLocation(time.DateTime, candidate.FailureTime, start.Location())
  44. if parseErr != nil {
  45. return nil, fmt.Errorf("invalid catalog failureTime %q: %w", candidate.FailureTime, parseErr)
  46. }
  47. if !failureTime.Before(start) && !failureTime.After(end) {
  48. selected = append(selected, candidate)
  49. }
  50. }
  51. if len(selected) == 0 {
  52. if item.CallRequestedAt != nil {
  53. return nil, fmt.Errorf("callable fault catalog became empty while call %d was active", item.CallRecordID)
  54. }
  55. return nil, nil
  56. }
  57. if err := ctx.Err(); err != nil {
  58. return nil, err
  59. }
  60. records, err := repositories.QueryFaultRecordList(item.DeviceSN, start.Format(time.DateTime), end.Format(time.DateTime))
  61. if err != nil {
  62. return nil, fmt.Errorf("query existing fault records: %w", err)
  63. }
  64. matched := make([]bool, len(selected))
  65. sources := make([]faultRecordSource, 0)
  66. for _, record := range records {
  67. if record.DeviceType != deviceType {
  68. continue
  69. }
  70. sn, identityErr := dataDownloadFaultRecordDeviceSN(record)
  71. if identityErr != nil {
  72. return sources, identityErr
  73. }
  74. if sn != item.DeviceSN {
  75. continue
  76. }
  77. if err := ctx.Err(); err != nil {
  78. return sources, err
  79. }
  80. subs, subErr := repositories.QueryFaultRecordSubList(record.Id)
  81. if subErr != nil {
  82. return sources, fmt.Errorf("list existing fault record %d: %w", record.Id, subErr)
  83. }
  84. if len(subs) != record.FaultRecordsCount {
  85. zap.L().Warn("[DataDownload] 已召唤录波子记录数量不一致,忽略该记录", zap.Int64("faultId", record.Id), zap.Int("expected", record.FaultRecordsCount), zap.Int("actual", len(subs)))
  86. }
  87. sources = append(sources, matchExistingFaultRecord(record, subs, selected, matched)...)
  88. }
  89. missing := make([]domain.FaultRecordCallableInfo, 0)
  90. indices := make([]int, 0)
  91. for i, candidate := range selected {
  92. if !matched[i] {
  93. missing = append(missing, candidate)
  94. indices = append(indices, candidate.Idx)
  95. }
  96. }
  97. if len(indices) == 0 {
  98. if item.CallRequestedAt != nil {
  99. if item.CallRecordStatus == nil || *item.CallRecordStatus != 2 {
  100. if _, err := summonFaultRecords(ctx, item, documentID, deviceType, nil); err != nil {
  101. return sources, err
  102. }
  103. }
  104. found := false
  105. for _, source := range sources {
  106. if source.record.Id == item.CallRecordID {
  107. found = true
  108. break
  109. }
  110. }
  111. if !found {
  112. return sources, fmt.Errorf("summoned fault record %d is not in the verified subrecords", item.CallRecordID)
  113. }
  114. }
  115. return sources, nil
  116. }
  117. called, err := summonFaultRecords(ctx, item, documentID, deviceType, indices)
  118. if err != nil {
  119. return sources, err
  120. }
  121. if err := ctx.Err(); err != nil {
  122. return sources, err
  123. }
  124. newRecords, err := repositories.QueryFaultRecordList(item.DeviceSN, start.Format(time.DateTime), end.Format(time.DateTime))
  125. if err != nil {
  126. return sources, fmt.Errorf("query summoned fault record %d: %w", called.Id, err)
  127. }
  128. var created *domain.FaultRecordInfo
  129. for i := range newRecords {
  130. if newRecords[i].Id == called.Id {
  131. created = &newRecords[i]
  132. break
  133. }
  134. }
  135. if created == nil {
  136. return sources, fmt.Errorf("summoned fault record %d is missing from faultRecordingLst", called.Id)
  137. }
  138. expected := item.CallRequestedCount
  139. if expected == 0 {
  140. expected = len(missing)
  141. }
  142. if created.FaultRecordsCount != expected {
  143. return sources, fmt.Errorf("summoned fault record %d count %d, expected %d", created.Id, created.FaultRecordsCount, expected)
  144. }
  145. subs, err := repositories.QueryFaultRecordSubList(created.Id)
  146. if err != nil {
  147. return sources, fmt.Errorf("list summoned fault record %d: %w", created.Id, err)
  148. }
  149. if len(subs) != expected {
  150. return sources, fmt.Errorf("summoned fault record %d has %d subrecords, expected %d", created.Id, len(subs), expected)
  151. }
  152. used := make([]bool, len(selected))
  153. for _, sub := range subs {
  154. match := -1
  155. for i, candidate := range selected {
  156. if !used[i] && faultSubMatchesCallable(sub, candidate) {
  157. match = i
  158. break
  159. }
  160. }
  161. if match < 0 {
  162. return sources, fmt.Errorf("summoned fault record %d contains unexpected subrecord %d", created.Id, sub.Id)
  163. }
  164. used[match] = true
  165. present := false
  166. for _, source := range sources {
  167. if source.record.Id == created.Id && source.sub.Id == sub.Id {
  168. present = true
  169. break
  170. }
  171. }
  172. if !present {
  173. sources = append(sources, faultRecordSource{record: *created, sub: sub})
  174. }
  175. }
  176. return sources, nil
  177. }
  178. func matchExistingFaultRecord(record domain.FaultRecordInfo, subs []domain.FaultRecordSubInfo, selected []domain.FaultRecordCallableInfo, matched []bool) []faultRecordSource {
  179. if len(subs) != record.FaultRecordsCount {
  180. return nil
  181. }
  182. var sources []faultRecordSource
  183. for _, sub := range subs {
  184. for i, candidate := range selected {
  185. if matched[i] || !faultSubMatchesCallable(sub, candidate) {
  186. continue
  187. }
  188. matched[i] = true
  189. sources = append(sources, faultRecordSource{record: record, sub: sub})
  190. break
  191. }
  192. }
  193. return sources
  194. }
  195. func faultSubMatchesCallable(sub domain.FaultRecordSubInfo, candidate domain.FaultRecordCallableInfo) bool {
  196. return strings.TrimSpace(sub.FailureTime) == strings.TrimSpace(candidate.FailureTime) &&
  197. strings.TrimSpace(sub.EventTriggerSource) == strings.TrimSpace(candidate.TriggerSourceCode)
  198. }
  199. func summonFaultRecords(ctx context.Context, item *domain.DataDownloadItem, documentID string, deviceType int, indices []int) (*domain.FaultRecordInfo, error) {
  200. requestedAt := item.CallRequestedAt
  201. if requestedAt != nil && !time.Now().Before(requestedAt.Add(faultCallTimeout)) {
  202. return nil, faultCallTimeoutError(item, callStatusText(item))
  203. }
  204. if requestedAt == nil {
  205. now := time.Now().UTC()
  206. requestedAt = &now
  207. }
  208. owned, err := mongoRepo.TryOwnDataDownloadFaultCall(ctx, documentID, item.ID, *requestedAt)
  209. if err != nil {
  210. return nil, err
  211. }
  212. if !owned {
  213. return nil, &DataDownloadFaultCallDeferred{After: faultCallPollInterval}
  214. }
  215. if item.CallRequestedAt == nil {
  216. if err := mongoRepo.UpdateDataDownloadFaultCall(ctx, item.ID, requestedAt, len(indices), 0, nil); err != nil {
  217. return nil, err
  218. }
  219. item.CallRequestedAt = requestedAt
  220. item.CallRequestedCount = len(indices)
  221. if err := ctx.Err(); err != nil {
  222. return nil, err
  223. }
  224. if err := repositories.QueryCellFaultRecord(deviceType, item.DeviceSN, indices); err != nil {
  225. return nil, fmt.Errorf("call fault recording: %w", err)
  226. }
  227. }
  228. deadline := requestedAt.Add(faultCallTimeout)
  229. pollCtx, cancel := context.WithDeadline(ctx, deadline)
  230. defer cancel()
  231. if err := pollCtx.Err(); err != nil {
  232. return nil, faultCallTimeoutError(item, callStatusText(item))
  233. }
  234. task, err := mongoRepo.GetDataDownloadTask(pollCtx, item.TaskID)
  235. if err != nil {
  236. return nil, err
  237. }
  238. if task.Status == domain.DataDownloadCancelling || task.Status == domain.DataDownloadCancelled {
  239. return nil, context.Canceled
  240. }
  241. var record *domain.FaultRecordInfo
  242. if item.CallRecordID != 0 {
  243. record, err = findFaultCallByID(pollCtx, item.CallRecordID)
  244. } else {
  245. record, err = findFaultCallByRequest(pollCtx, item.DeviceSN, deviceType, item.CallRequestedCount, *requestedAt)
  246. }
  247. if errors.Is(err, context.DeadlineExceeded) {
  248. return nil, faultCallTimeoutError(item, callStatusText(item))
  249. }
  250. if errors.Is(err, errAmbiguousFaultCall) {
  251. return nil, err
  252. }
  253. if err != nil {
  254. zap.L().Warn("[DataDownload] 录波召唤状态查询失败,下一轮重试",
  255. zap.String("itemId", item.ID.Hex()), zap.Int64("recordId", item.CallRecordID), zap.Error(err))
  256. }
  257. if record != nil {
  258. item.CallRecordID = record.Id
  259. item.CallRecordStatus = record.Status
  260. if err := mongoRepo.UpdateDataDownloadFaultCall(ctx, item.ID, nil, 0, record.Id, record.Status); err != nil {
  261. return nil, err
  262. }
  263. if record.Status != nil {
  264. switch *record.Status {
  265. case 2:
  266. return record, nil
  267. case -1:
  268. return nil, fmt.Errorf("%w: recordId=%d reason=%s", ErrFaultRecordCallFailed, record.Id, faultReason(record))
  269. }
  270. }
  271. }
  272. remaining := time.Until(deadline)
  273. if remaining <= 0 {
  274. return nil, faultCallTimeoutError(item, callStatusText(item))
  275. }
  276. if remaining > faultCallPollInterval {
  277. remaining = faultCallPollInterval
  278. }
  279. return nil, &DataDownloadFaultCallDeferred{After: remaining}
  280. }
  281. func callStatusText(item *domain.DataDownloadItem) string {
  282. if item.CallRecordStatus == nil {
  283. return "unknown"
  284. }
  285. return strconv.Itoa(*item.CallRecordStatus)
  286. }
  287. func faultReason(record *domain.FaultRecordInfo) string {
  288. if record.FaultReason == nil {
  289. return "upstream reported failure"
  290. }
  291. return *record.FaultReason
  292. }
  293. func findFaultCallByRequest(ctx context.Context, sn string, deviceType, count int, requestedAt time.Time) (*domain.FaultRecordInfo, error) {
  294. var found *domain.FaultRecordInfo
  295. lowerTime := requestedAt.Add(-time.Minute)
  296. for page := 1; page <= 200; page++ {
  297. if err := ctx.Err(); err != nil {
  298. return nil, err
  299. }
  300. records, err := repositories.QueryFaultRecordCallRecordPageContext(ctx, page, faultCallPageSize)
  301. if err != nil {
  302. return nil, err
  303. }
  304. for i := range records {
  305. record := &records[i]
  306. if record.DeviceType != deviceType || record.FaultRecordsCount != count || record.TriggerType != 2 {
  307. continue
  308. }
  309. deviceSN, snErr := dataDownloadFaultRecordDeviceSN(*record)
  310. if snErr != nil || deviceSN != sn {
  311. continue
  312. }
  313. callTime, parseErr := time.ParseInLocation(time.DateTime, record.CellTime, time.UTC)
  314. if parseErr != nil || callTime.Before(requestedAt.Add(-time.Minute)) || callTime.After(requestedAt.Add(time.Minute)) {
  315. continue
  316. }
  317. if found != nil && found.Id != record.Id {
  318. return nil, fmt.Errorf("%w for %s at %s", errAmbiguousFaultCall, sn, requestedAt.Format(time.RFC3339))
  319. }
  320. copy := *record
  321. found = &copy
  322. }
  323. // IDs are returned newest first. Once the oldest row is older than the
  324. // match window, later pages cannot contain this request.
  325. if len(records) > 0 {
  326. oldest, parseErr := time.ParseInLocation(time.DateTime, records[len(records)-1].CellTime, time.UTC)
  327. if parseErr == nil && oldest.Before(lowerTime) {
  328. return found, nil
  329. }
  330. }
  331. if len(records) < faultCallPageSize {
  332. return found, nil
  333. }
  334. }
  335. return nil, fmt.Errorf("fault recording call search exceeded 200 pages")
  336. }
  337. func findFaultCallByID(ctx context.Context, id int64) (*domain.FaultRecordInfo, error) {
  338. probe := func(page int) (*domain.FaultRecordInfo, bool, error) {
  339. if err := ctx.Err(); err != nil {
  340. return nil, false, err
  341. }
  342. records, err := repositories.QueryFaultRecordCallRecordPageContext(ctx, page, faultCallPageSize)
  343. if err != nil {
  344. return nil, false, err
  345. }
  346. for i := range records {
  347. if records[i].Id == id {
  348. return &records[i], false, nil
  349. }
  350. }
  351. return nil, len(records) > 0 && records[len(records)-1].Id > id, nil
  352. }
  353. // Exponential probing bounds the target page; binary search then locates
  354. // it without scanning every newer page on each 60-second poll.
  355. lower, upper := 0, 1
  356. for upper <= 1024 {
  357. record, newer, err := probe(upper)
  358. if err != nil {
  359. return nil, err
  360. }
  361. if record != nil {
  362. return record, nil
  363. }
  364. if !newer {
  365. break
  366. }
  367. lower, upper = upper, upper*2
  368. }
  369. if upper > 1024 {
  370. return nil, fmt.Errorf("fault recording call %d is older than 1024 pages", id)
  371. }
  372. for lower+1 < upper {
  373. middle := lower + (upper-lower)/2
  374. record, newer, err := probe(middle)
  375. if err != nil {
  376. return nil, err
  377. }
  378. if record != nil {
  379. return record, nil
  380. }
  381. if newer {
  382. lower = middle
  383. } else {
  384. upper = middle
  385. }
  386. }
  387. return nil, fmt.Errorf("fault recording call %d is absent from lstFaultWaveCall", id)
  388. }