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