|
|
@@ -0,0 +1,94 @@
|
|
|
+package services
|
|
|
+
|
|
|
+import (
|
|
|
+ "ems-backend/db"
|
|
|
+ "log"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ writeChanSize = 10000
|
|
|
+ writeBatchSize = 500
|
|
|
+ writeFlushInterval = 100 * time.Millisecond
|
|
|
+)
|
|
|
+
|
|
|
+var GlobalTDengineWriter *TDengineWriter
|
|
|
+
|
|
|
+// TDengineWriter asynchronously batches and writes readings to TDengine via channel + goroutine.
|
|
|
+// Decouples collection from write, reducing blocking risk during collection cycles.
|
|
|
+type TDengineWriter struct {
|
|
|
+ ch chan []db.ReadingRecord
|
|
|
+ wg sync.WaitGroup
|
|
|
+}
|
|
|
+
|
|
|
+// NewTDengineWriter creates and returns a new TDengineWriter.
|
|
|
+func NewTDengineWriter() *TDengineWriter {
|
|
|
+ return &TDengineWriter{
|
|
|
+ ch: make(chan []db.ReadingRecord, 64),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Start starts the background writer goroutine.
|
|
|
+func (w *TDengineWriter) Start() {
|
|
|
+ w.wg.Add(1)
|
|
|
+ go w.run()
|
|
|
+ log.Println("TDengineWriter started (async batch write)")
|
|
|
+}
|
|
|
+
|
|
|
+// Stop gracefully stops the writer: closes the channel, flushes remaining data, then returns.
|
|
|
+func (w *TDengineWriter) Stop() {
|
|
|
+ close(w.ch)
|
|
|
+ w.wg.Wait()
|
|
|
+ log.Println("TDengineWriter stopped")
|
|
|
+}
|
|
|
+
|
|
|
+// Write sends a batch of records to the writer. Non-blocking; drops with log if channel is full.
|
|
|
+func (w *TDengineWriter) Write(records []db.ReadingRecord) {
|
|
|
+ if len(records) == 0 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ select {
|
|
|
+ case w.ch <- records:
|
|
|
+ default:
|
|
|
+ log.Printf("TDengineWriter: channel full, dropping %d records", len(records))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (w *TDengineWriter) run() {
|
|
|
+ defer w.wg.Done()
|
|
|
+ ticker := time.NewTicker(writeFlushInterval)
|
|
|
+ defer ticker.Stop()
|
|
|
+
|
|
|
+ pending := make([]db.ReadingRecord, 0, writeBatchSize*4)
|
|
|
+
|
|
|
+ flush := func() {
|
|
|
+ if len(pending) == 0 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := db.BatchInsertReadings(pending); err != nil {
|
|
|
+ log.Printf("TDengineWriter: batch insert failed (%d records): %v", len(pending), err)
|
|
|
+ }
|
|
|
+ pending = pending[:0]
|
|
|
+ }
|
|
|
+
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case batch, ok := <-w.ch:
|
|
|
+ if !ok {
|
|
|
+ flush()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ pending = append(pending, batch...)
|
|
|
+ for len(pending) >= writeBatchSize {
|
|
|
+ chunk := pending[:writeBatchSize]
|
|
|
+ pending = pending[writeBatchSize:]
|
|
|
+ if err := db.BatchInsertReadings(chunk); err != nil {
|
|
|
+ log.Printf("TDengineWriter: batch insert failed (%d records): %v", len(chunk), err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case <-ticker.C:
|
|
|
+ flush()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|