github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/rpc/nats/subscriber_fastcache.go (about) 1 package nats 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "path/filepath" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 "sync/atomic" 15 "syscall" 16 "time" 17 18 "github.com/angenalZZZ/gofunc/data/cache/fastcache" 19 "github.com/angenalZZZ/gofunc/f" 20 "github.com/nats-io/nats.go" 21 ) 22 23 // SubscriberFastCache The NatS Subscriber with Fast Cache Temporary Storage. 24 type SubscriberFastCache struct { 25 *nats.Conn 26 sub *nats.Subscription 27 Subj string 28 Hand func([][]byte) error 29 *fastcache.Cache 30 CacheDir string // sets cache persist to disk directory 31 Index uint64 32 Count uint64 33 Since *f.TimeStamp 34 MsgLimit int // sets the limits for pending messages for this subscription. 35 BytesLimit int // sets the limits for a message's bytes for this subscription. 36 OnceAmount int64 // sets amount allocated at one time 37 OnceInterval time.Duration 38 Running bool 39 async bool 40 err error 41 pool *f.Pool 42 } 43 44 // NewSubscriberFastCache Create a subscriber with cache store for Client Connect. 45 func NewSubscriberFastCache(nc *nats.Conn, subject string, cacheDir ...string) *SubscriberFastCache { 46 dir, since := ".nats", f.TimeFrom(time.Now(), true) 47 48 if len(cacheDir) == 1 && cacheDir[0] != "" { 49 if err := f.Mkdir(cacheDir[0]); err != nil { 50 return nil 51 } 52 dir = filepath.Join(cacheDir[0], since.LocalTimeStampString(true)) 53 } else { 54 if err := f.MkdirCurrent(dir); err != nil { 55 return nil 56 } 57 dir = filepath.Join(f.CurrentDir(), dir, since.LocalTimeStampString(true)) 58 } 59 60 client := fastcache.New(1073741824) // 1GB cache capacity 61 62 sub := &SubscriberFastCache{ 63 Conn: nc, 64 Subj: subject, 65 Cache: client, // fast cache 66 CacheDir: dir, 67 Since: since, 68 MsgLimit: 100000000, // pending messages: 100 million 69 BytesLimit: 1048576, // a message's size: 1MB 70 OnceAmount: -1, 71 OnceInterval: time.Second, 72 async: true, 73 } 74 75 sub.pool = f.NewPool(f.NumCPUx16, func() f.PoolWorker { 76 return &CachePoolWorker{ 77 processor: sub.Process, 78 } 79 }) 80 81 return sub 82 } 83 84 // LimitMessage sets amount for pending messages for this subscription, and a message's bytes. 85 // Defaults amountPendingMessages: 100 million, anMessageBytes: 1MB 86 func (sub *SubscriberFastCache) LimitMessage(amountPendingMessages, anMessageBytes int) { 87 sub.MsgLimit, sub.BytesLimit = amountPendingMessages, anMessageBytes 88 } 89 90 // LimitAmount sets amount allocated at one time, and the processing interval time. 91 // Defaults onceAmount: -1, onceInterval: time.Second 92 func (sub *SubscriberFastCache) LimitAmount(onceAmount int64, onceInterval time.Duration) { 93 sub.OnceAmount, sub.OnceInterval = onceAmount, onceInterval 94 } 95 96 // Process messages for this subscription. 97 func (sub *SubscriberFastCache) Process(msg *CacheMsg) error { 98 key, val := msg.Key, msg.Val 99 sub.Cache.Set(f.BytesUint64(key), val) 100 return nil 101 } 102 103 // Run runtime to end your application. 104 func (sub *SubscriberFastCache) Run(waitFunc ...func()) { 105 sub.Running = true 106 ctx, cancel := context.WithCancel(context.Background()) 107 108 // Handle panic. 109 defer func() { 110 err := recover() 111 if err != nil { 112 Log.Error().Msgf("[nats] stop receive new data with error.panic > %v", err) 113 } else { 114 Log.Warn().Msgf("[nats] stop receive new data > %d/%d < %d records not processed", sub.Index, sub.Count, sub.Count-sub.Index) 115 } 116 117 // Unsubscribe will remove interest in the given subject. 118 _ = sub.sub.Unsubscribe() 119 // Drain connection (Preferred for responders), Close() not needed if this is called. 120 _ = sub.Conn.Drain() 121 122 // Stop handle new data. 123 cancel() 124 // Stop pool processor. 125 sub.pool.Close() 126 // Save not processed data. 127 sub.Save(sub.CacheDir) 128 129 // os.Exit(1) 130 if err != nil { 131 log.Fatal(err) 132 } 133 sub.Running = false 134 }() 135 136 // Async Subscriber. 137 sub.sub, sub.err = sub.Conn.Subscribe(sub.Subj, func(msg *nats.Msg) { 138 key, val := atomic.AddUint64(&sub.Count, 1), msg.Data 139 //sub.pool.Process(&CacheMsg{Key: key, Val: val}) // It's slow 140 sub.Cache.Set(f.BytesUint64(key), val) 141 }) 142 // Set listening. 143 SubscribeErrorHandle(sub.sub, sub.async, sub.err) 144 if sub.err != nil { 145 log.Fatal(sub.err) 146 } 147 148 // Set pending limits. 149 SubscribeLimitHandle(sub.sub, sub.MsgLimit, sub.BytesLimit) 150 151 // Flush connection to server, returns when all messages have been processed. 152 FlushAndCheckLastError(sub.Conn) 153 154 // init handle old data. 155 go sub.init(ctx) 156 157 // run handle new data. 158 go sub.hand(ctx) 159 160 if len(waitFunc) > 0 { 161 Log.Info().Msg("[nats] start running and wait to auto exit") 162 waitFunc[0]() 163 return 164 } 165 166 Log.Info().Msg("[nats] start running and wait to manual exit") 167 168 // Pass the signals you want to end your application. 169 death := f.NewDeath(syscall.SIGINT, syscall.SIGTERM) 170 // When you want to block for shutdown signals. 171 death.WaitForDeathWithFunc(func() { 172 Log.Warn().Msg("[nats] forced to shutdown.") 173 }) 174 } 175 176 // Stop runtime to end your application. 177 func (sub *SubscriberFastCache) Stop(ms ...int) { 178 n := 10000 179 if len(ms) > 0 { 180 n = ms[0] 181 } 182 for ; sub.Running && n > 0; n-- { 183 time.Sleep(time.Millisecond) 184 } 185 } 186 187 // init handle old data. 188 func (sub *SubscriberFastCache) init(ctx context.Context) { 189 if sub.Hand == nil { 190 return 191 } 192 193 cacheDir := sub.CacheDir 194 dir0, parentDir := filepath.Base(cacheDir), filepath.Dir(cacheDir) 195 oldDirs, err := filepath.Glob(filepath.Join(parentDir, "*")) 196 if err != nil || oldDirs == nil || len(oldDirs) == 0 { 197 return 198 } 199 sort.Strings(oldDirs) 200 201 var clearCache = func(cache *fastcache.Cache, index, count int64) { 202 for i, c := uint64(index)+1, uint64(count); i <= c; i++ { 203 k := f.BytesUint64(i) 204 cache.Del(k) 205 } 206 } 207 208 handRecords, oldRecords, onceRecords := 0, 0, atomic.LoadInt64(&sub.OnceAmount) 209 var runHandle = func(dir string, dirname string) (ok bool) { 210 oldFiles, err := filepath.Glob(filepath.Join(dir, "*")) 211 if err != nil || oldFiles == nil || len(oldFiles) == 0 { 212 ok = true 213 return 214 } 215 sort.Strings(oldFiles) 216 for _, oldFile := range oldFiles { 217 _, jsonFile := filepath.Split(oldFile) 218 if ok, _ := regexp.MatchString(`^\d+\.\d+\.\d+\.json$`, jsonFile); !ok { 219 continue 220 } 221 222 fileDir := strings.Replace(jsonFile, ".json", "", 1) 223 filePath := filepath.Join(dir, fileDir) 224 if f.PathExists(filePath) == false { 225 continue 226 } 227 228 cache, err := fastcache.LoadFromFile(filePath) 229 s := strings.Split(fileDir, ".") 230 if err != nil || len(s) != 3 { 231 continue 232 } 233 234 since := s[0] 235 index, _ := strconv.ParseInt(s[1], 10, 0) 236 count, _ := strconv.ParseInt(s[2], 10, 0) 237 indexZero, indexSize := uint64(index)+1, count-index 238 if onceRecords > 0 { 239 indexSize = onceRecords 240 } 241 242 var handData = make([][]byte, 0, indexSize) 243 for i, c, dataIndex := indexZero, uint64(count), int64(0); i <= c; i++ { 244 if key := f.BytesUint64(i); cache.Has(key) { 245 val := cache.Get(nil, key) 246 handData = append(handData, val) 247 if dataIndex++; dataIndex == onceRecords || i == c { 248 // bulk handle 249 if err := sub.Hand(handData); err != nil { 250 // rollback 251 Log.Error().Msgf("[nats] init handle old data > %s > %s", dirname, err) 252 if i > indexZero { 253 clearCache(cache, int64(indexZero)-1, int64(i)) 254 dirname1, filename1 := sub.dirnames(since, i-1, c), sub.filenames(since, i-1, c) 255 saveFastCache(cache, dir, dirname1, filename1) 256 _ = os.Remove(oldFile) 257 _ = os.RemoveAll(filePath) 258 } 259 return 260 } 261 handRecords += len(handData) 262 // reset data 263 dataIndex = 0 264 handData = make([][]byte, 0, indexSize) 265 time.Sleep(sub.OnceInterval) 266 } 267 } 268 } 269 270 _ = os.Remove(oldFile) 271 _ = os.RemoveAll(filePath) 272 } 273 274 ok = true 275 return 276 } 277 278 for _, oldDir := range oldDirs { 279 if !f.IsDir(oldDir) { 280 continue 281 } 282 283 dir1 := filepath.Base(oldDir) 284 if ok, _ := regexp.MatchString(`^\d+$`, dir1); !ok { 285 continue 286 } 287 if len(dir1) != 14 || dir1 == dir0 { 288 continue 289 } 290 if datFiles, err := filepath.Glob(filepath.Join(oldDir, "*.json")); err != nil || datFiles == nil || len(datFiles) == 0 { 291 continue 292 } 293 294 // reboot init handle old data 295 for f.PathExists(oldDir) { 296 if runHandle(oldDir, dir1) { 297 if err1 := os.RemoveAll(oldDir); err1 != nil { 298 Log.Error().Msgf("[nats] remove old data directory > %s > %s", dir1, err1) 299 } 300 } 301 Log.Info().Msgf("[nats] init handle old data > %d records < %s", handRecords, dir1) 302 oldRecords += handRecords 303 handRecords = 0 304 } 305 } 306 307 if oldRecords > 0 { 308 Log.Info().Msgf("[nats] init handle old data > %d records", oldRecords) 309 } 310 311 if err := ctx.Err(); err != nil && err != context.Canceled { 312 Log.Warn().Msgf("[nats] init handle old data err > %s", err) 313 } 314 } 315 316 // run handle new data. 317 func (sub *SubscriberFastCache) hand(ctx context.Context) { 318 if sub.Hand == nil { 319 return 320 } 321 322 var ( 323 running bool 324 runCount uint64 325 delIndex uint64 326 ) 327 328 var runHandle = func() { 329 count, index := atomic.LoadUint64(&sub.Count), atomic.LoadUint64(&sub.Index) 330 if count <= runCount { 331 // reset handle 332 if 0 < count && 3 == time.Now().Hour() && index <= delIndex { 333 sub.Count, runCount, sub.Index, delIndex = 0, 0, 0, 0 334 } 335 return 336 } 337 338 handRecords, onceRecords := int64(0), atomic.LoadInt64(&sub.OnceAmount) 339 indexSize := int64(count - index) 340 if onceRecords > 0 { 341 indexSize = onceRecords 342 } 343 344 var handData = make([][]byte, 0, indexSize) 345 for dataIndex := int64(0); dataIndex < indexSize && index < count; runCount++ { 346 index++ // key equals index 347 val := sub.Cache.Get(nil, f.BytesUint64(index)) 348 if val == nil { 349 val = []byte{} 350 } 351 handData = append(handData, val) 352 handRecords++ 353 if dataIndex++; dataIndex == indexSize || index == count { 354 // bulk handle 355 if err := sub.Hand(handData); err != nil { 356 // rollback 357 Log.Error().Msgf("[nats] run handle new data err > %s", err) 358 handRecords -= dataIndex 359 break 360 } 361 sub.Index += uint64(dataIndex) 362 // reset data 363 dataIndex = 0 364 handData = make([][]byte, 0, indexSize) 365 } 366 } 367 368 if handRecords == 0 { 369 return 370 } 371 372 Log.Info().Msgf("[nats] run handle new data > %d/%d < %d records", sub.Index, sub.Count, handRecords) 373 374 // delete old data 375 go func(index, handRecords uint64) { 376 for i, n := index-handRecords+1, index; i <= n; i++ { 377 sub.Cache.Del(f.BytesUint64(i)) 378 atomic.AddUint64(&delIndex, 1) 379 } 380 }(sub.Index, uint64(handRecords)) 381 } 382 383 for { 384 select { 385 case <-ctx.Done(): 386 if err := ctx.Err(); err != nil && err != context.Canceled { 387 Log.Warn().Msgf("[nats] done handle new data err > %s", err) 388 } 389 for running { 390 time.Sleep(time.Millisecond) 391 } 392 runHandle() 393 return 394 case <-time.After(sub.OnceInterval): 395 if running { 396 continue 397 } 398 running = true 399 Log.Debug().Msgf("[nats] run receive new data > %d/%d", sub.Index, sub.Count) 400 runHandle() 401 running = false 402 } 403 } 404 } 405 406 // Dirname gets the Cache Dirname. 407 func (sub *SubscriberFastCache) Dirname() string { 408 return sub.dirname(sub.Since, sub.Index, sub.Count) 409 } 410 411 func (sub *SubscriberFastCache) dirname(since *f.TimeStamp, index, count uint64) string { 412 return sub.dirnames(since.LocalTimeStampString(true), index, count) 413 } 414 415 func (sub *SubscriberFastCache) dirnames(since string, index, count uint64) string { 416 return fmt.Sprintf("%s.%d.%d", since, index, count) 417 } 418 419 // Filename gets the Cache Filename. 420 func (sub *SubscriberFastCache) Filename() string { 421 return sub.filename(sub.Since, sub.Index, sub.Count) 422 } 423 424 func (sub *SubscriberFastCache) filename(since *f.TimeStamp, index, count uint64) string { 425 return sub.filenames(since.LocalTimeStampString(true), index, count) 426 } 427 428 func (sub *SubscriberFastCache) filenames(since string, index, count uint64) string { 429 return fmt.Sprintf("%s.%d.%d.json", since, index, count) 430 } 431 432 // Save the Cache Data of some records not processed. 433 func (sub *SubscriberFastCache) Save(cacheDir string) { 434 if sub.Count == 0 || sub.Hand == nil || sub.Index == sub.Count { 435 return 436 } 437 438 saveFastCache(sub.Cache, cacheDir, sub.Dirname(), sub.Filename()) 439 } 440 441 func saveFastCache(cache *fastcache.Cache, cacheDir, dirname, filename string) { 442 fileStat := new(fastcache.Stats) 443 cache.UpdateStats(fileStat) 444 handData, err := f.EncodeJson(fileStat) 445 if err != nil { 446 Log.Error().Msgf("[nats] save cache stats > %s", err) 447 } 448 449 filePath := filepath.Join(cacheDir, filename) 450 err = ioutil.WriteFile(filePath, handData, 0644) 451 if err != nil { 452 Log.Error().Msgf("[nats] save cache stats > %s", err) 453 } 454 455 dirPath := filepath.Join(cacheDir, dirname) 456 if err = cache.SaveToFileConcurrent(dirPath, 0); err != nil { 457 Log.Error().Msgf("[nats] save cache data > %s", err) 458 } else { 459 cache.Reset() // Reset removes all the items from the cache. 460 } 461 }