mirror of
https://github.com/luxfi/database.git
synced 2026-10-30 15:01:18 +00:00
201 lines
4.6 KiB
Go
201 lines
4.6 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package hookdb
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// IndexerNotifier sends database events to an indexer service
|
|
type IndexerNotifier struct {
|
|
endpoint string
|
|
client *http.Client
|
|
|
|
// Batching
|
|
mu sync.Mutex
|
|
batch []IndexerEvent
|
|
batchMax int
|
|
ticker *time.Ticker
|
|
done chan struct{}
|
|
}
|
|
|
|
// IndexerEvent is sent to the indexer service
|
|
type IndexerEvent struct {
|
|
ChainID string `json:"chain_id"`
|
|
Type string `json:"type"` // "put" or "delete"
|
|
Prefix string `json:"prefix"`
|
|
Key string `json:"key"`
|
|
Value []byte `json:"value,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// NotifierConfig configures the indexer notifier
|
|
type NotifierConfig struct {
|
|
// Endpoint is the indexer service URL (e.g., "http://localhost:8090/api/v2/events")
|
|
Endpoint string
|
|
// ChainID identifies this chain
|
|
ChainID string
|
|
// BatchSize is the max events per batch (default: 100)
|
|
BatchSize int
|
|
// FlushInterval is how often to flush batches (default: 100ms)
|
|
FlushInterval time.Duration
|
|
// Timeout for HTTP requests (default: 5s)
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// NewNotifier creates a new indexer notifier
|
|
func NewNotifier(cfg NotifierConfig) *IndexerNotifier {
|
|
if cfg.BatchSize <= 0 {
|
|
cfg.BatchSize = 100
|
|
}
|
|
if cfg.FlushInterval <= 0 {
|
|
cfg.FlushInterval = 100 * time.Millisecond
|
|
}
|
|
if cfg.Timeout <= 0 {
|
|
cfg.Timeout = 5 * time.Second
|
|
}
|
|
|
|
n := &IndexerNotifier{
|
|
endpoint: cfg.Endpoint,
|
|
client: &http.Client{Timeout: cfg.Timeout},
|
|
batchMax: cfg.BatchSize,
|
|
batch: make([]IndexerEvent, 0, cfg.BatchSize),
|
|
ticker: time.NewTicker(cfg.FlushInterval),
|
|
done: make(chan struct{}),
|
|
}
|
|
|
|
go n.flusher()
|
|
return n
|
|
}
|
|
|
|
// flusher periodically flushes batched events
|
|
func (n *IndexerNotifier) flusher() {
|
|
for {
|
|
select {
|
|
case <-n.ticker.C:
|
|
n.flush()
|
|
case <-n.done:
|
|
n.flush() // Final flush
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// flush sends batched events to the indexer
|
|
func (n *IndexerNotifier) flush() {
|
|
n.mu.Lock()
|
|
if len(n.batch) == 0 {
|
|
n.mu.Unlock()
|
|
return
|
|
}
|
|
events := n.batch
|
|
n.batch = make([]IndexerEvent, 0, n.batchMax)
|
|
n.mu.Unlock()
|
|
|
|
// Send to indexer
|
|
if err := n.send(events); err != nil {
|
|
log.Printf("[hookdb] Failed to send %d events to indexer: %v", len(events), err)
|
|
}
|
|
}
|
|
|
|
// send posts events to the indexer service
|
|
func (n *IndexerNotifier) send(events []IndexerEvent) error {
|
|
if n.endpoint == "" {
|
|
return nil // No endpoint configured
|
|
}
|
|
|
|
data, err := json.Marshal(events)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal events: %w", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", n.endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := n.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("indexer returned status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handler returns a hook handler for a specific chain
|
|
func (n *IndexerNotifier) Handler(chainID string) Handler {
|
|
return func(e Event) {
|
|
eventType := "put"
|
|
if e.Type == EventDelete {
|
|
eventType = "delete"
|
|
}
|
|
|
|
event := IndexerEvent{
|
|
ChainID: chainID,
|
|
Type: eventType,
|
|
Prefix: string(e.Prefix),
|
|
Key: string(e.Key),
|
|
Value: e.Value,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
n.mu.Lock()
|
|
n.batch = append(n.batch, event)
|
|
shouldFlush := len(n.batch) >= n.batchMax
|
|
n.mu.Unlock()
|
|
|
|
if shouldFlush {
|
|
go n.flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close stops the notifier and flushes remaining events
|
|
func (n *IndexerNotifier) Close() {
|
|
n.ticker.Stop()
|
|
close(n.done)
|
|
}
|
|
|
|
// BlockPrefixes returns common block-related key prefixes to watch
|
|
func BlockPrefixes() [][]byte {
|
|
return [][]byte{
|
|
[]byte("b"), // blocks
|
|
[]byte("H"), // block headers
|
|
[]byte("h"), // block hashes
|
|
[]byte("n"), // block numbers
|
|
[]byte("l"), // transaction lookups
|
|
[]byte("r"), // receipts
|
|
}
|
|
}
|
|
|
|
// EVMPrefixes returns EVM-specific key prefixes to watch
|
|
func EVMPrefixes() [][]byte {
|
|
return [][]byte{
|
|
[]byte("B"), // block body
|
|
[]byte("H"), // block header
|
|
[]byte("h"), // header hash
|
|
[]byte("n"), // header number
|
|
[]byte("b"), // block hash -> number
|
|
[]byte("l"), // tx lookup
|
|
[]byte("r"), // receipts
|
|
[]byte("LastBlock"), // last block
|
|
[]byte("LastAccepted"), // last accepted
|
|
}
|
|
}
|