github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/redis_streams.go (about) 1 package writer 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 ibatch "github.com/Jeffail/benthos/v3/internal/batch" 10 bredis "github.com/Jeffail/benthos/v3/internal/impl/redis" 11 "github.com/Jeffail/benthos/v3/internal/metadata" 12 "github.com/Jeffail/benthos/v3/lib/log" 13 "github.com/Jeffail/benthos/v3/lib/message/batch" 14 "github.com/Jeffail/benthos/v3/lib/metrics" 15 "github.com/Jeffail/benthos/v3/lib/types" 16 "github.com/go-redis/redis/v7" 17 ) 18 19 //------------------------------------------------------------------------------ 20 21 // RedisStreamsConfig contains configuration fields for the RedisStreams output type. 22 type RedisStreamsConfig struct { 23 bredis.Config `json:",inline" yaml:",inline"` 24 Stream string `json:"stream" yaml:"stream"` 25 BodyKey string `json:"body_key" yaml:"body_key"` 26 MaxLenApprox int64 `json:"max_length" yaml:"max_length"` 27 MaxInFlight int `json:"max_in_flight" yaml:"max_in_flight"` 28 Metadata metadata.ExcludeFilterConfig `json:"metadata" yaml:"metadata"` 29 Batching batch.PolicyConfig `json:"batching" yaml:"batching"` 30 } 31 32 // NewRedisStreamsConfig creates a new RedisStreamsConfig with default values. 33 func NewRedisStreamsConfig() RedisStreamsConfig { 34 return RedisStreamsConfig{ 35 Config: bredis.NewConfig(), 36 Stream: "benthos_stream", 37 BodyKey: "body", 38 MaxLenApprox: 0, 39 MaxInFlight: 1, 40 Metadata: metadata.NewExcludeFilterConfig(), 41 Batching: batch.NewPolicyConfig(), 42 } 43 } 44 45 //------------------------------------------------------------------------------ 46 47 // RedisStreams is an output type that serves RedisStreams messages. 48 type RedisStreams struct { 49 log log.Modular 50 stats metrics.Type 51 52 conf RedisStreamsConfig 53 metaFilter *metadata.ExcludeFilter 54 55 client redis.UniversalClient 56 connMut sync.RWMutex 57 } 58 59 // NewRedisStreams creates a new RedisStreams output type. 60 func NewRedisStreams( 61 conf RedisStreamsConfig, 62 log log.Modular, 63 stats metrics.Type, 64 ) (*RedisStreams, error) { 65 66 r := &RedisStreams{ 67 log: log, 68 stats: stats, 69 conf: conf, 70 } 71 72 var err error 73 if r.metaFilter, err = conf.Metadata.Filter(); err != nil { 74 return nil, fmt.Errorf("failed to construct metadata filter: %w", err) 75 } 76 77 if _, err = conf.Config.Client(); err != nil { 78 return nil, err 79 } 80 return r, nil 81 } 82 83 //------------------------------------------------------------------------------ 84 85 // ConnectWithContext establishes a connection to an RedisStreams server. 86 func (r *RedisStreams) ConnectWithContext(ctx context.Context) error { 87 return r.Connect() 88 } 89 90 // Connect establishes a connection to an RedisStreams server. 91 func (r *RedisStreams) Connect() error { 92 r.connMut.Lock() 93 defer r.connMut.Unlock() 94 95 client, err := r.conf.Config.Client() 96 if err != nil { 97 return err 98 } 99 if _, err = client.Ping().Result(); err != nil { 100 return err 101 } 102 103 r.log.Infof("Pushing messages to Redis stream: %v\n", r.conf.Stream) 104 105 r.client = client 106 return nil 107 } 108 109 //------------------------------------------------------------------------------ 110 111 // WriteWithContext attempts to write a message by pushing it to a Redis stream. 112 func (r *RedisStreams) WriteWithContext(ctx context.Context, msg types.Message) error { 113 return r.Write(msg) 114 } 115 116 // Write attempts to write a message by pushing it to a Redis stream. 117 func (r *RedisStreams) Write(msg types.Message) error { 118 r.connMut.RLock() 119 client := r.client 120 r.connMut.RUnlock() 121 122 if client == nil { 123 return types.ErrNotConnected 124 } 125 126 partToMap := func(p types.Part) map[string]interface{} { 127 values := map[string]interface{}{} 128 r.metaFilter.Iter(p.Metadata(), func(k, v string) error { 129 values[k] = v 130 return nil 131 }) 132 values[r.conf.BodyKey] = p.Get() 133 return values 134 } 135 136 if msg.Len() == 1 { 137 if err := client.XAdd(&redis.XAddArgs{ 138 ID: "*", 139 Stream: r.conf.Stream, 140 MaxLenApprox: r.conf.MaxLenApprox, 141 Values: partToMap(msg.Get(0)), 142 }).Err(); err != nil { 143 r.disconnect() 144 r.log.Errorf("Error from redis: %v\n", err) 145 return types.ErrNotConnected 146 } 147 return nil 148 } 149 150 pipe := client.Pipeline() 151 msg.Iter(func(i int, p types.Part) error { 152 _ = pipe.XAdd(&redis.XAddArgs{ 153 ID: "*", 154 Stream: r.conf.Stream, 155 MaxLenApprox: r.conf.MaxLenApprox, 156 Values: partToMap(p), 157 }) 158 return nil 159 }) 160 cmders, err := pipe.Exec() 161 if err != nil { 162 r.disconnect() 163 r.log.Errorf("Error from redis: %v\n", err) 164 return types.ErrNotConnected 165 } 166 167 var batchErr *ibatch.Error 168 for i, res := range cmders { 169 if res.Err() != nil { 170 if batchErr == nil { 171 batchErr = ibatch.NewError(msg, res.Err()) 172 } 173 batchErr.Failed(i, res.Err()) 174 } 175 } 176 if batchErr != nil { 177 return batchErr 178 } 179 return nil 180 } 181 182 // disconnect safely closes a connection to an RedisStreams server. 183 func (r *RedisStreams) disconnect() error { 184 r.connMut.Lock() 185 defer r.connMut.Unlock() 186 if r.client != nil { 187 err := r.client.Close() 188 r.client = nil 189 return err 190 } 191 return nil 192 } 193 194 // CloseAsync shuts down the RedisStreams output and stops processing messages. 195 func (r *RedisStreams) CloseAsync() { 196 r.disconnect() 197 } 198 199 // WaitForClose blocks until the RedisStreams output has closed down. 200 func (r *RedisStreams) WaitForClose(timeout time.Duration) error { 201 return nil 202 } 203 204 //------------------------------------------------------------------------------