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  //------------------------------------------------------------------------------