github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/rpc/nats/subscriber_fast.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" 19 "github.com/angenalZZZ/gofunc/f" 20 "github.com/nats-io/nats.go" 21 ) 22 23 // SubscriberFast The NatS Subscriber with Fast Concurrent Map Temporary Storage. 24 type SubscriberFast struct { 25 *nats.Conn 26 sub *nats.Subscription 27 Subj string 28 Hand func([][]byte) error 29 Cache f.CiMap 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 async bool 39 err error 40 } 41 42 // NewSubscriberFast Create a subscriber with cache store for Client Connect. 43 func NewSubscriberFast(nc *nats.Conn, subject string, cacheDir ...string) *SubscriberFast { 44 sub := &SubscriberFast{ 45 Conn: nc, 46 Subj: subject, 47 Cache: f.NewCiMap(), // fast Concurrent Map 48 Since: f.TimeFrom(time.Now(), true), 49 MsgLimit: 100000000, // pending messages: 100 million 50 BytesLimit: 1048576, // a message's size: 1MB 51 OnceAmount: -1, 52 OnceInterval: time.Second, 53 async: true, 54 } 55 if len(cacheDir) == 1 && cacheDir[0] != "" { 56 sub.CacheDir = cacheDir[0] 57 } else { 58 sub.CacheDir = data.CurrentDir 59 } 60 return sub 61 } 62 63 // LimitMessage sets amount for pending messages for this subscription, and a message's bytes. 64 // Defaults amountPendingMessages: 100 million, anMessageBytes: 1MB 65 func (sub *SubscriberFast) LimitMessage(amountPendingMessages, anMessageBytes int) { 66 sub.MsgLimit, sub.BytesLimit = amountPendingMessages, anMessageBytes 67 } 68 69 // LimitAmount sets amount allocated at one time, and the processing interval time. 70 // Defaults onceAmount: -1, onceInterval: time.Second 71 func (sub *SubscriberFast) LimitAmount(onceAmount int64, onceInterval time.Duration) { 72 sub.OnceAmount, sub.OnceInterval = onceAmount, onceInterval 73 } 74 75 // Run runtime to end your application. 76 func (sub *SubscriberFast) Run(waitFunc ...func()) { 77 ctx, cancel := context.WithCancel(context.Background()) 78 79 // Handle panic. 80 defer func() { 81 err := recover() 82 if err != nil { 83 Log.Error().Msgf("[nats] stop receive new data with error.panic > %v", err) 84 } else { 85 Log.Warn().Msgf("[nats] stop receive new data > %d/%d < %d records not processed", sub.Index, sub.Count, sub.Count-sub.Index) 86 } 87 88 // Unsubscribe will remove interest in the given subject. 89 _ = sub.sub.Unsubscribe() 90 // Drain connection (Preferred for responders), Close() not needed if this is called. 91 _ = sub.Conn.Drain() 92 93 // Stop handle new data. 94 cancel() 95 // Save cache. 96 sub.Save(sub.CacheDir) 97 98 // os.Exit(1) 99 if err != nil { 100 log.Fatal(err) 101 } 102 }() 103 104 // Async Subscriber. 105 sub.sub, sub.err = sub.Conn.Subscribe(sub.Subj, func(msg *nats.Msg) { 106 key, val := atomic.AddUint64(&sub.Count, 1), msg.Data 107 sub.Cache.Set(key, val) 108 }) 109 // Set listening. 110 SubscribeErrorHandle(sub.sub, sub.async, sub.err) 111 if sub.err != nil { 112 log.Fatal(sub.err) 113 } 114 115 // Set pending limits. 116 SubscribeLimitHandle(sub.sub, sub.MsgLimit, sub.BytesLimit) 117 118 // Flush connection to server, returns when all messages have been processed. 119 FlushAndCheckLastError(sub.Conn) 120 121 // init handle old data. 122 go sub.init(ctx) 123 124 // run handle new data. 125 go sub.hand(ctx) 126 127 if len(waitFunc) > 0 { 128 Log.Info().Msg("[nats] start running and wait to auto exit") 129 waitFunc[0]() 130 return 131 } 132 133 Log.Info().Msg("[nats] start running and wait to manual exit") 134 135 // Pass the signals you want to end your application. 136 death := f.NewDeath(syscall.SIGINT, syscall.SIGTERM) 137 // When you want to block for shutdown signals. 138 death.WaitForDeathWithFunc(func() { 139 Log.Warn().Msg("[nats] forced to shutdown.") 140 }) 141 } 142 143 // init handle old data. 144 func (sub *SubscriberFast) init(ctx context.Context) { 145 if sub.Hand == nil { 146 return 147 } 148 149 oldFiles, _ := filepath.Glob(filepath.Join(sub.CacheDir, "*")) 150 sort.Strings(oldFiles) 151 152 cacheDir := sub.CacheDir 153 var clearCache = func(cache f.CiMap, index, count int64) { 154 for i, c := uint64(index)+1, uint64(count); i <= c; i++ { 155 cache.Remove(i) 156 } 157 } 158 159 handRecords, onceRecords := 0, atomic.LoadInt64(&sub.OnceAmount) 160 for _, oldFile := range oldFiles { 161 _, jsonFile := filepath.Split(oldFile) 162 if ok, _ := regexp.MatchString(`^\d+\.\d+\.\d+\.json$`, jsonFile); !ok { 163 continue 164 } 165 166 dirname := strings.ReplaceAll(jsonFile, ".json", "") 167 fileData, err := ioutil.ReadFile(oldFile) 168 if err != nil { 169 continue 170 } 171 172 cache, err := f.NewCiMapFromJSON(fileData) 173 s := strings.Split(dirname, ".") 174 if err != nil || len(s) != 3 { 175 continue 176 } 177 178 since := s[0] 179 index, _ := strconv.ParseInt(s[1], 10, 0) 180 count, _ := strconv.ParseInt(s[2], 10, 0) 181 indexZero, indexSize := uint64(index)+1, count-index 182 if onceRecords > 0 { 183 indexSize = onceRecords 184 } 185 186 var handData = make([][]byte, 0, indexSize) 187 for i, c, dataIndex := indexZero, uint64(count), int64(0); i <= c; i++ { 188 if val, ok := cache.Get(i); ok { 189 handData = append(handData, val.([]byte)) 190 if dataIndex++; dataIndex == onceRecords || i == c { 191 // bulk handle 192 if err := sub.Hand(handData); err != nil { 193 // rollback 194 Log.Error().Msgf("[nats] init handle old data > %s > %s", dirname, err) 195 if i > indexZero { 196 clearCache(cache, int64(indexZero)-1, int64(i)) 197 dirname1, filename1 := sub.dirnames(since, i-1, c), sub.filenames(since, i-1, c) 198 saveFastMap(cache, cacheDir, dirname1, filename1) 199 _ = os.Remove(oldFile) 200 } 201 // reboot init handle old data 202 time.Sleep(sub.OnceInterval) 203 sub.init(ctx) 204 return 205 } 206 handRecords += len(handData) 207 // reset data 208 dataIndex = 0 209 handData = make([][]byte, 0, indexSize) 210 time.Sleep(sub.OnceInterval) 211 } 212 } 213 } 214 215 _ = os.Remove(oldFile) 216 } 217 218 Log.Info().Msgf("[nats] init handle old data > %d records", handRecords) 219 if err := ctx.Err(); err != nil && err != context.Canceled { 220 Log.Warn().Msgf("[nats] init handle old data err > %s", err) 221 } 222 } 223 224 // run handle new data. 225 func (sub *SubscriberFast) hand(ctx context.Context) { 226 if sub.Hand == nil { 227 return 228 } 229 230 var ( 231 running bool 232 runCount uint64 233 delIndex uint64 234 ) 235 236 var runHandle = func() { 237 count, index := atomic.LoadUint64(&sub.Count), atomic.LoadUint64(&sub.Index) 238 if count <= runCount { 239 // reset handle 240 if 0 < count && 3 == time.Now().Hour() && index <= delIndex { 241 sub.Count, runCount, sub.Index, delIndex = 0, 0, 0, 0 242 } 243 return 244 } 245 246 handRecords, onceRecords := int64(0), atomic.LoadInt64(&sub.OnceAmount) 247 indexSize := int64(count - index) 248 if onceRecords > 0 { 249 indexSize = onceRecords 250 } 251 252 var handData = make([][]byte, 0, indexSize) 253 for dataIndex := int64(0); dataIndex < indexSize && index < count; runCount++ { 254 index++ // key equals index 255 val, ok := sub.Cache.Get(index) 256 if !ok || val == nil { 257 val = []byte{} 258 } 259 handData = append(handData, val.([]byte)) 260 handRecords++ 261 if dataIndex++; dataIndex == indexSize || index == count { 262 // bulk handle 263 if err := sub.Hand(handData); err != nil { 264 // rollback 265 Log.Error().Msgf("[nats] run handle new data err > %s", err) 266 handRecords -= dataIndex 267 break 268 } 269 sub.Index += uint64(dataIndex) 270 // reset data 271 dataIndex = 0 272 handData = make([][]byte, 0, indexSize) 273 } 274 } 275 276 if handRecords == 0 { 277 return 278 } 279 280 Log.Info().Msgf("[nats] run handle new data > %d/%d < %d records", sub.Index, sub.Count, handRecords) 281 282 // delete old data 283 go func(index, handRecords uint64) { 284 for i, n := index-handRecords+1, index; i <= n; i++ { 285 sub.Cache.Remove(i) 286 atomic.AddUint64(&delIndex, 1) 287 } 288 }(sub.Index, uint64(handRecords)) 289 } 290 291 for { 292 select { 293 case <-ctx.Done(): 294 if err := ctx.Err(); err != nil && err != context.Canceled { 295 Log.Warn().Msgf("[nats] done handle new data err > %s", err) 296 } 297 for running { 298 time.Sleep(time.Millisecond) 299 } 300 runHandle() 301 return 302 case <-time.After(sub.OnceInterval): 303 if running { 304 continue 305 } 306 running = true 307 Log.Debug().Msgf("[nats] run receive new data > %d/%d", sub.Index, sub.Count) 308 runHandle() 309 running = false 310 } 311 } 312 } 313 314 // Dirname gets the Cache Dirname. 315 func (sub *SubscriberFast) Dirname() string { 316 return sub.dirname(sub.Since, sub.Index, sub.Count) 317 } 318 319 func (sub *SubscriberFast) dirname(since *f.TimeStamp, index, count uint64) string { 320 return sub.dirnames(since.LocalTimeStampString(true), index, count) 321 } 322 323 func (sub *SubscriberFast) dirnames(since string, index, count uint64) string { 324 return fmt.Sprintf("%s.%d.%d", since, index, count) 325 } 326 327 // Filename gets the Cache Filename. 328 func (sub *SubscriberFast) Filename() string { 329 return sub.filename(sub.Since, sub.Index, sub.Count) 330 } 331 332 func (sub *SubscriberFast) filename(since *f.TimeStamp, index, count uint64) string { 333 return sub.filenames(since.LocalTimeStampString(true), index, count) 334 } 335 336 func (sub *SubscriberFast) filenames(since string, index, count uint64) string { 337 return fmt.Sprintf("%s.%d.%d.json", since, index, count) 338 } 339 340 // Save the Cache Data of some records not processed. 341 func (sub *SubscriberFast) Save(cacheDir string) { 342 if sub.Count == 0 || sub.Hand == nil || sub.Index == sub.Count { 343 return 344 } 345 346 saveFastMap(sub.Cache, cacheDir, sub.Dirname(), sub.Filename()) 347 } 348 349 func saveFastMap(cache f.CiMap, cacheDir, dirname, filename string) { 350 handData, err := cache.JSON() 351 if err != nil { 352 Log.Error().Msgf("[nats] save cache stats > %s", err) 353 } 354 355 filePath := filepath.Join(cacheDir, filename) 356 err = ioutil.WriteFile(filePath, handData, 0644) 357 if err != nil { 358 Log.Error().Msgf("[nats] save cache stats > %s", err) 359 } 360 }