github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/redis_hash.go (about) 1 package writer 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/Jeffail/benthos/v3/internal/bloblang/field" 11 bredis "github.com/Jeffail/benthos/v3/internal/impl/redis" 12 "github.com/Jeffail/benthos/v3/internal/interop" 13 "github.com/Jeffail/benthos/v3/lib/log" 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 // RedisHashConfig contains configuration fields for the RedisHash output type. 22 type RedisHashConfig struct { 23 bredis.Config `json:",inline" yaml:",inline"` 24 Key string `json:"key" yaml:"key"` 25 WalkMetadata bool `json:"walk_metadata" yaml:"walk_metadata"` 26 WalkJSONObject bool `json:"walk_json_object" yaml:"walk_json_object"` 27 Fields map[string]string `json:"fields" yaml:"fields"` 28 MaxInFlight int `json:"max_in_flight" yaml:"max_in_flight"` 29 } 30 31 // NewRedisHashConfig creates a new RedisHashConfig with default values. 32 func NewRedisHashConfig() RedisHashConfig { 33 return RedisHashConfig{ 34 Config: bredis.NewConfig(), 35 Key: "", 36 WalkMetadata: false, 37 WalkJSONObject: false, 38 Fields: map[string]string{}, 39 MaxInFlight: 1, 40 } 41 } 42 43 //------------------------------------------------------------------------------ 44 45 // RedisHash is an output type that writes hash objects to Redis using the HMSET 46 // command. 47 type RedisHash struct { 48 log log.Modular 49 stats metrics.Type 50 51 conf RedisHashConfig 52 53 keyStr *field.Expression 54 fields map[string]*field.Expression 55 56 client redis.UniversalClient 57 connMut sync.RWMutex 58 } 59 60 // NewRedisHash creates a new RedisHash output type. 61 // 62 // Deprecated: use the V2 API instead. 63 func NewRedisHash( 64 conf RedisHashConfig, 65 log log.Modular, 66 stats metrics.Type, 67 ) (*RedisHash, error) { 68 return NewRedisHashV2(conf, types.NoopMgr(), log, stats) 69 } 70 71 // NewRedisHashV2 creates a new RedisHash output type. 72 func NewRedisHashV2( 73 conf RedisHashConfig, 74 mgr types.Manager, 75 log log.Modular, 76 stats metrics.Type, 77 ) (*RedisHash, error) { 78 r := &RedisHash{ 79 log: log, 80 stats: stats, 81 conf: conf, 82 fields: map[string]*field.Expression{}, 83 } 84 85 var err error 86 if r.keyStr, err = interop.NewBloblangField(mgr, conf.Key); err != nil { 87 return nil, fmt.Errorf("failed to parse key expression: %v", err) 88 } 89 90 for k, v := range conf.Fields { 91 if r.fields[k], err = interop.NewBloblangField(mgr, v); err != nil { 92 return nil, fmt.Errorf("failed to parse field '%v' expression: %v", k, err) 93 } 94 } 95 96 if !conf.WalkMetadata && !conf.WalkJSONObject && len(conf.Fields) == 0 { 97 return nil, errors.New("at least one mechanism for setting fields must be enabled") 98 } 99 100 if _, err := conf.Config.Client(); err != nil { 101 return nil, err 102 } 103 104 return r, nil 105 } 106 107 //------------------------------------------------------------------------------ 108 109 // ConnectWithContext establishes a connection to an RedisHash server. 110 func (r *RedisHash) ConnectWithContext(ctx context.Context) error { 111 return r.Connect() 112 } 113 114 // Connect establishes a connection to an RedisHash server. 115 func (r *RedisHash) Connect() error { 116 r.connMut.Lock() 117 defer r.connMut.Unlock() 118 119 client, err := r.conf.Config.Client() 120 if err != nil { 121 return err 122 } 123 if _, err = client.Ping().Result(); err != nil { 124 return err 125 } 126 127 r.log.Infoln("Setting messages as hash objects to Redis") 128 129 r.client = client 130 return nil 131 } 132 133 //------------------------------------------------------------------------------ 134 135 func walkForHashFields( 136 msg types.Message, index int, fields map[string]interface{}, 137 ) error { 138 jVal, err := msg.Get(index).JSON() 139 if err != nil { 140 return err 141 } 142 jObj, ok := jVal.(map[string]interface{}) 143 if !ok { 144 return fmt.Errorf("expected JSON object, found '%T'", jVal) 145 } 146 for k, v := range jObj { 147 fields[k] = v 148 } 149 return nil 150 } 151 152 // WriteWithContext attempts to write a message to Redis by setting it using the 153 // HMSET command. 154 func (r *RedisHash) WriteWithContext(ctx context.Context, msg types.Message) error { 155 return r.Write(msg) 156 } 157 158 // Write attempts to write a message to Redis by setting it using the HMSET 159 // command. 160 func (r *RedisHash) Write(msg types.Message) error { 161 r.connMut.RLock() 162 client := r.client 163 r.connMut.RUnlock() 164 165 if client == nil { 166 return types.ErrNotConnected 167 } 168 169 return IterateBatchedSend(msg, func(i int, p types.Part) error { 170 key := r.keyStr.String(i, msg) 171 fields := map[string]interface{}{} 172 if r.conf.WalkMetadata { 173 p.Metadata().Iter(func(k, v string) error { 174 fields[k] = v 175 return nil 176 }) 177 } 178 if r.conf.WalkJSONObject { 179 if err := walkForHashFields(msg, i, fields); err != nil { 180 err = fmt.Errorf("failed to walk JSON object: %v", err) 181 r.log.Errorf("HMSET error: %v\n", err) 182 return err 183 } 184 } 185 for k, v := range r.fields { 186 fields[k] = v.String(i, msg) 187 } 188 if err := client.HMSet(key, fields).Err(); err != nil { 189 r.disconnect() 190 r.log.Errorf("Error from redis: %v\n", err) 191 return types.ErrNotConnected 192 } 193 return nil 194 }) 195 } 196 197 // disconnect safely closes a connection to an RedisHash server. 198 func (r *RedisHash) disconnect() error { 199 r.connMut.Lock() 200 defer r.connMut.Unlock() 201 if r.client != nil { 202 err := r.client.Close() 203 r.client = nil 204 return err 205 } 206 return nil 207 } 208 209 // CloseAsync shuts down the RedisHash output and stops processing messages. 210 func (r *RedisHash) CloseAsync() { 211 r.disconnect() 212 } 213 214 // WaitForClose blocks until the RedisHash output has closed down. 215 func (r *RedisHash) WaitForClose(timeout time.Duration) error { 216 return nil 217 } 218 219 //------------------------------------------------------------------------------