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