table_queue.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. package dbQueue
  2. import (
  3. "context"
  4. "f1-game/internal/extend/maps"
  5. "f1-game/internal/pb"
  6. "runtime/debug"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. "go.mongodb.org/mongo-driver/v2/bson"
  11. "go.mongodb.org/mongo-driver/v2/mongo"
  12. mopts "go.mongodb.org/mongo-driver/v2/mongo/options"
  13. dbFacade "f1-game/internal/component/db/facade"
  14. cstring "github.com/cherry-game/cherry/extend/string"
  15. clog "github.com/cherry-game/cherry/logger"
  16. )
  17. type (
  18. TableQueue struct {
  19. workers map[string]*tableWorker // worker map
  20. db *mongo.Database // gorm db
  21. onStopScanSecond time.Duration // 停机时扫描表等待时间
  22. producerWg sync.WaitGroup // 追踪 run (生产者)
  23. isRun bool // 是否启动
  24. }
  25. tableWorker struct {
  26. options
  27. tableName string // 表名
  28. dataMaps *maps.OrderedMap[any, *mongo.UpdateOneModel] // 所有待保存的数据 key:hash, value:*Table
  29. submitCount int64 // 提交到保存队列的总数
  30. mongoUpdateOpts *mopts.FindOneAndUpdateOptionsBuilder // mongodb update options
  31. }
  32. options struct {
  33. db *mongo.Database // mongodb
  34. saveChanSize int // chan的大小
  35. saveTicker time.Duration // 默认保存频率(秒)
  36. saveCount int // 默认每次保存的数量
  37. onStopSaveTicker time.Duration // 停机时保存频率(秒)
  38. onStopSaveCount int // 停机时每次保存的数量
  39. printSaveLog bool // 是否打印保存日志
  40. }
  41. OptionFunc func(options *options)
  42. )
  43. func newOption(db *mongo.Database) options {
  44. return options{
  45. db: db,
  46. saveChanSize: 10240,
  47. saveTicker: 60 * time.Second,
  48. saveCount: 500,
  49. onStopSaveTicker: 1 * time.Second,
  50. onStopSaveCount: 1000,
  51. printSaveLog: false,
  52. }
  53. }
  54. func NewTableQueue(db *mongo.Database) *TableQueue {
  55. if db == nil {
  56. clog.Fatal("db is nil")
  57. return nil
  58. }
  59. queue := &TableQueue{
  60. db: db,
  61. workers: make(map[string]*tableWorker),
  62. onStopScanSecond: 3 * time.Second,
  63. isRun: false,
  64. }
  65. return queue
  66. }
  67. func (p *TableQueue) AddTable(table dbFacade.Table, optsFunc ...OptionFunc) {
  68. if p.isRun {
  69. clog.Warn("TableQueue already started")
  70. return
  71. }
  72. if table == nil {
  73. clog.Warn("table is nil")
  74. return
  75. }
  76. if cstring.IsBlank(table.TableName()) {
  77. clog.Warn("table name is empty.")
  78. return
  79. }
  80. if _, found := p.workers[table.TableName()]; found {
  81. clog.Warnf("duplicate table = %s name.", table.TableName())
  82. return
  83. }
  84. opts := newOption(p.db)
  85. for _, optionFunc := range optsFunc {
  86. optionFunc(&opts)
  87. }
  88. worker := &tableWorker{
  89. options: opts,
  90. tableName: table.TableName(),
  91. dataMaps: maps.NewOrderMap[any, *mongo.UpdateOneModel](opts.saveChanSize, 10),
  92. submitCount: 0,
  93. mongoUpdateOpts: mopts.FindOneAndUpdate().SetUpsert(true), // set upsert
  94. }
  95. p.workers[worker.tableName] = worker
  96. clog.Infof("add [table = %s, ticker = %d(sec), count = %d]",
  97. worker.tableName,
  98. int(worker.saveTicker.Seconds()),
  99. worker.saveCount,
  100. )
  101. }
  102. func (p *TableQueue) AddTables(tables []dbFacade.Table, optsFunc ...OptionFunc) {
  103. for _, table := range tables {
  104. p.AddTable(table, optsFunc...)
  105. }
  106. }
  107. func (p *TableQueue) Start() {
  108. if p.isRun {
  109. clog.Warn("TableQueue already started")
  110. return
  111. }
  112. p.isRun = true
  113. clog.Infof("db queue starting...")
  114. for _, table := range p.workers {
  115. p.producerWg.Add(1)
  116. go table.run(&p.producerWg)
  117. }
  118. }
  119. func (p *TableQueue) Stop() {
  120. clog.Infof("stopping db queue....")
  121. for _, tw := range p.workers {
  122. tw.saveTicker = tw.onStopSaveTicker
  123. tw.saveCount = tw.onStopSaveCount
  124. }
  125. // 阶段 1: 等待所有内存数据刷入
  126. for {
  127. if m := p.allWorkerDataCount(); m < 1 {
  128. break
  129. }
  130. // wait...
  131. clog.Debugf("saving data! waiting in %d seconds.", int(p.onStopScanSecond.Seconds()))
  132. time.Sleep(p.onStopScanSecond)
  133. }
  134. clog.Infof("stopping db queue finish!")
  135. // 阶段 2: 关闭数据源 (这会触发 run 协程退出)
  136. for _, worker := range p.workers {
  137. worker.dataMaps.Close()
  138. }
  139. // 阶段 3: 等待所有生产者 (run 协程) 退出
  140. p.producerWg.Wait()
  141. }
  142. // 对象进入队列
  143. func (p *TableQueue) InQueue(table dbFacade.Table) {
  144. worker, found := p.workers[table.TableName()]
  145. if !found {
  146. clog.Errorf("[table = %s] unregister!", table.TableName())
  147. return
  148. }
  149. // flush update time
  150. table.FlushUpdateTime(time.Now().UnixMilli())
  151. table.CheckField()
  152. // marshal to bytes
  153. tableBytes, err := bson.Marshal(table)
  154. if err != nil {
  155. clog.Warnf("[table = %s] Marshal table to bytes error. [err = %v]", table.TableName(), err)
  156. return
  157. }
  158. // set update
  159. update := bson.D{{Key: "$set", Value: bson.Raw(tableBytes)}}
  160. // set filter
  161. filter := bson.M{
  162. "_id": table.PrimaryValue(), // 利用自带的_id主键进行条件过滤
  163. }
  164. // build *mongo.UpdateOneModel
  165. model := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true)
  166. worker.dataMaps.Set(table.PrimaryValue(), model)
  167. }
  168. func (p *TableQueue) allWorkerDataCount() int {
  169. var allDataMapCount = 0
  170. for _, worker := range p.workers {
  171. allDataMapCount += worker.dataMaps.Len()
  172. }
  173. return allDataMapCount
  174. }
  175. func (p *TableQueue) allSubmitCount() int64 {
  176. var allSubmitCount int64 = 0
  177. for _, worker := range p.workers {
  178. allSubmitCount += worker.getSubmitCount()
  179. }
  180. return allSubmitCount
  181. }
  182. func (p *TableQueue) Report() *pb.DBQueueReportList {
  183. list := &pb.DBQueueReportList{}
  184. for _, worker := range p.workers {
  185. list.List = append(list.List, &pb.DBQueueReport{
  186. TableName: worker.tableName,
  187. RemainCount: int64(worker.dataMaps.Len()),
  188. SubmitCount: worker.getSubmitCount(),
  189. })
  190. }
  191. return list
  192. }
  193. func (p *tableWorker) run(wg *sync.WaitGroup) {
  194. defer wg.Done()
  195. clog.Infof("run queue for [table = %s]", p.tableName)
  196. checkTicker := time.NewTicker(100 * time.Millisecond)
  197. defer checkTicker.Stop()
  198. lastTicker := time.Now()
  199. for {
  200. select {
  201. case <-p.dataMaps.GetStopChan():
  202. return
  203. case t := <-checkTicker.C:
  204. if t.Unix() >= lastTicker.Add(p.saveTicker).Unix() {
  205. lastTicker = time.Now()
  206. // Use PopBatch to get multiple items at once
  207. list := p.dataMaps.PopBatch(p.saveCount)
  208. listLen := len(list)
  209. if listLen > 0 {
  210. p.submitIncrease(listLen)
  211. p.bulkWrite(list)
  212. p.submitDecrease(listLen)
  213. }
  214. }
  215. }
  216. }
  217. }
  218. func (p *tableWorker) bulkWrite(list []*mongo.UpdateOneModel) {
  219. defer func() {
  220. if r := recover(); r != nil {
  221. clog.Warnf("[table = %s] recover in executor. %s", p.tableName, string(debug.Stack()))
  222. for _, model := range list {
  223. clog.Warnf("%+v", model)
  224. }
  225. }
  226. }()
  227. // 构建批量更新对象
  228. models := make([]mongo.WriteModel, 0, len(list))
  229. for _, model := range list {
  230. models = append(models, model)
  231. }
  232. bulkWriteOptions := mopts.BulkWrite().SetOrdered(false)
  233. _, err := p.db.Collection(p.tableName).BulkWrite(context.TODO(), models, bulkWriteOptions)
  234. if err != nil && err != mongo.ErrNoDocuments {
  235. // ignore error: no document
  236. clog.Warnf("[table = %s] save error. [error = %v]", p.tableName, err)
  237. for _, model := range list {
  238. clog.Warnf("%+v", model)
  239. }
  240. } else {
  241. clog.Debugf("[table = %s, size = %d] queue bulk write.", p.tableName, len(list))
  242. }
  243. }
  244. func (p *tableWorker) submitIncrease(num int) {
  245. atomic.AddInt64(&p.submitCount, int64(num))
  246. }
  247. func (p *tableWorker) submitDecrease(num int) {
  248. atomic.AddInt64(&p.submitCount, -int64(num))
  249. }
  250. func (p *tableWorker) getSubmitCount() int64 {
  251. return atomic.LoadInt64(&p.submitCount)
  252. }
  253. func SetChanSize(saveChanSize int) OptionFunc {
  254. return func(options *options) {
  255. if saveChanSize > 0 {
  256. options.saveChanSize = saveChanSize
  257. }
  258. }
  259. }
  260. func SetTicker(saveTicker time.Duration, saveCount int) OptionFunc {
  261. return func(options *options) {
  262. if saveTicker > 10*time.Millisecond {
  263. options.saveTicker = saveTicker
  264. }
  265. if saveCount > 0 {
  266. options.saveCount = saveCount
  267. }
  268. }
  269. }
  270. func SetOnStopTicker(onStopSaveTicker time.Duration, onStopSaveCount int) OptionFunc {
  271. return func(options *options) {
  272. if onStopSaveTicker > 10*time.Millisecond {
  273. options.onStopSaveTicker = onStopSaveTicker
  274. }
  275. if onStopSaveCount > 0 {
  276. options.onStopSaveCount = onStopSaveCount
  277. }
  278. }
  279. }
  280. func SetPrintSaveLog(printSaveLog bool) OptionFunc {
  281. return func(options *options) {
  282. options.printSaveLog = printSaveLog
  283. }
  284. }