github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/modules/metrics/clickhouse.go (about) 1 package metrics 2 3 import ( 4 "context" 5 "errors" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/ClickHouse/clickhouse-go/v2" 11 "github.com/ClickHouse/clickhouse-go/v2/lib/driver" 12 13 "github.com/machinefi/w3bstream/pkg/depends/conf/logger" 14 "github.com/machinefi/w3bstream/pkg/types" 15 ) 16 17 const ( 18 queueLength = 5000 19 popThreshold = 3 20 concurrentWorker = 10 21 ) 22 23 type ( 24 ClickhouseClient struct { 25 workerPool []*connWorker 26 sqLQueue chan *queueElement 27 } 28 29 queueElement struct { 30 query string 31 count int 32 } 33 ) 34 35 var ( 36 clickhouseCLI *ClickhouseClient 37 sleepTime = 10 * time.Second 38 ) 39 40 func Init(ctx context.Context) { 41 cfg, existed := types.MetricsCenterConfigFromContext(ctx) 42 if !existed || len(cfg.ClickHouseDSN) == 0 { 43 log.Println("fail to get the config of metrics center") 44 return 45 } 46 opts, err := clickhouse.ParseDSN(cfg.ClickHouseDSN) 47 if err != nil { 48 panic(err) 49 } 50 { 51 opts.Settings["async_insert"] = 1 52 opts.Settings["wait_for_async_insert"] = 0 53 opts.Settings["async_insert_busy_timeout_ms"] = 100 54 } 55 clickhouseCLI = newClickhouseClient(opts) 56 log.Println("clickhouse client is initialized") 57 } 58 59 func newClickhouseClient(cfg *clickhouse.Options) *ClickhouseClient { 60 cc := &ClickhouseClient{ 61 sqLQueue: make(chan *queueElement, queueLength), 62 } 63 for i := 0; i < concurrentWorker; i++ { 64 cc.workerPool = append(cc.workerPool, &connWorker{ 65 sqLQueue: cc.sqLQueue, 66 cfg: cfg, 67 }) 68 go cc.workerPool[i].run() 69 } 70 return cc 71 } 72 73 func (c *ClickhouseClient) Insert(query string) error { 74 select { 75 case c.sqLQueue <- &queueElement{ 76 query: query, 77 count: 0, 78 }: 79 default: 80 return errors.New("the queue of client is full") 81 } 82 return nil 83 } 84 85 type SQLBatcher struct { 86 signal chan string 87 preStatm string 88 buf []string 89 } 90 91 const ( 92 batchSize = 50000 93 tickerInterval = 200 * time.Millisecond 94 ) 95 96 func NewSQLBatcher(preStatm string) *SQLBatcher { 97 bw := &SQLBatcher{ 98 signal: make(chan string, queueLength), 99 preStatm: preStatm, 100 buf: make([]string, 0, batchSize), 101 } 102 go bw.run() 103 return bw 104 } 105 106 func (b *SQLBatcher) Insert(query string) error { 107 _, l := logger.NewSpanContext(context.Background(), "metrics.SQLBatcher.Insert") 108 defer l.End() 109 110 if clickhouseCLI == nil { 111 return errors.New("clickhouse client is not initialized") 112 } 113 select { 114 case b.signal <- query: 115 return nil 116 default: 117 return errors.New("the queue of SQLBatcher is full") 118 } 119 } 120 121 func (b *SQLBatcher) run() { 122 ticker := time.NewTicker(tickerInterval) 123 for { 124 select { 125 case <-ticker.C: 126 if len(b.buf) == 0 { 127 continue 128 } 129 if clickhouseCLI == nil { 130 log.Println("clickhouse client is not initialized") 131 continue 132 } 133 _ = b.insert() 134 case str, ok := <-b.signal: 135 if !ok { 136 return 137 } 138 if clickhouseCLI == nil { 139 log.Println("clickhouse client is not initialized") 140 continue 141 } 142 b.buf = append(b.buf, str) 143 if len(b.buf) >= batchSize { 144 if b.insert() != nil { 145 continue 146 } 147 ticker.Reset(tickerInterval) 148 } 149 } 150 } 151 } 152 153 func (b *SQLBatcher) insert() error { 154 _, l := logger.NewSpanContext(context.Background(), "metrics.SQLBatcher.insert") 155 defer l.End() 156 157 err := clickhouseCLI.Insert(b.preStatm + "(" + strings.Join(b.buf, "),(") + ")") 158 if err != nil { 159 l.Error(err) 160 return err 161 } 162 b.buf = b.buf[0:0] 163 return nil 164 } 165 166 type connWorker struct { 167 sqLQueue chan *queueElement 168 conn driver.Conn 169 cfg *clickhouse.Options 170 } 171 172 func (c *connWorker) run() { 173 for { 174 if err := c.connect(); err != nil { 175 log.Println("ClickhouseClient failed to connect: ", err) 176 time.Sleep(sleepTime) 177 continue 178 } 179 ele := <-c.sqLQueue 180 if err := c.conn.Exec(context.Background(), ele.query); err != nil { 181 if !c.liveness() { 182 c.conn = nil 183 log.Printf("ClickhouseClient failed to connect the server: error: %s, query %s\n", err, ele.query) 184 } else { 185 log.Printf("ClickhouseClient failed to insert data: error %s, query %s\n ", err, ele.query) 186 } 187 if ele.count > popThreshold { 188 log.Printf("the query %s in ClickhouseClient is poped due to %d times failure.", ele.query, ele.count) 189 continue 190 } 191 ele.count++ 192 // TODO: Double linked list should be used to append the element to the head 193 // when the order of the queue is important 194 c.sqLQueue <- ele 195 } 196 } 197 } 198 199 func (c *connWorker) connect() error { 200 if c.conn != nil { 201 return nil 202 } 203 conn, err := clickhouse.Open(c.cfg) 204 if err != nil { 205 return err 206 } 207 c.conn = conn 208 if !c.liveness() { 209 c.conn = nil 210 return errors.New("failed to ping clickhouse server") 211 } 212 log.Println("clickhouse server login successfully") 213 return nil 214 } 215 216 func (c *connWorker) liveness() bool { 217 if err := c.conn.Ping(context.Background()); err != nil { 218 log.Println("failed to ping clickhouse server: ", err) 219 return false 220 } 221 return true 222 }