github.com/Jeffail/benthos/v3@v3.65.0/lib/message/batch/policy.go (about)

     1  package batch
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/Jeffail/benthos/v3/internal/bloblang/mapping"
     9  	"github.com/Jeffail/benthos/v3/internal/interop"
    10  	"github.com/Jeffail/benthos/v3/lib/condition"
    11  	"github.com/Jeffail/benthos/v3/lib/log"
    12  	"github.com/Jeffail/benthos/v3/lib/message"
    13  	"github.com/Jeffail/benthos/v3/lib/metrics"
    14  	"github.com/Jeffail/benthos/v3/lib/processor"
    15  	"github.com/Jeffail/benthos/v3/lib/types"
    16  )
    17  
    18  // SanitisePolicyConfig returns a policy config structure ready to be marshalled
    19  // with irrelevant fields omitted.
    20  func SanitisePolicyConfig(policy PolicyConfig) (interface{}, error) {
    21  	procConfs := make([]interface{}, len(policy.Processors))
    22  	for i, pConf := range policy.Processors {
    23  		var err error
    24  		if procConfs[i], err = processor.SanitiseConfig(pConf); err != nil {
    25  			return nil, err
    26  		}
    27  	}
    28  	bSanit := map[string]interface{}{
    29  		"byte_size":  policy.ByteSize,
    30  		"count":      policy.Count,
    31  		"check":      policy.Check,
    32  		"period":     policy.Period,
    33  		"processors": procConfs,
    34  	}
    35  	if !isNoopCondition(policy.Condition) {
    36  		condSanit, err := condition.SanitiseConfig(policy.Condition)
    37  		if err != nil {
    38  			return nil, err
    39  		}
    40  		bSanit["condition"] = condSanit
    41  	}
    42  	return bSanit, nil
    43  }
    44  
    45  //------------------------------------------------------------------------------
    46  
    47  func isNoopCondition(conf condition.Config) bool {
    48  	return conf.Type == condition.TypeStatic && !conf.Static
    49  }
    50  
    51  // PolicyConfig contains configuration parameters for a batch policy.
    52  type PolicyConfig struct {
    53  	ByteSize   int                `json:"byte_size" yaml:"byte_size"`
    54  	Count      int                `json:"count" yaml:"count"`
    55  	Condition  condition.Config   `json:"condition" yaml:"condition"`
    56  	Check      string             `json:"check" yaml:"check"`
    57  	Period     string             `json:"period" yaml:"period"`
    58  	Processors []processor.Config `json:"processors" yaml:"processors"`
    59  }
    60  
    61  // NewPolicyConfig creates a default PolicyConfig.
    62  func NewPolicyConfig() PolicyConfig {
    63  	cond := condition.NewConfig()
    64  	cond.Type = "static"
    65  	cond.Static = false
    66  	return PolicyConfig{
    67  		ByteSize:   0,
    68  		Count:      0,
    69  		Condition:  cond,
    70  		Check:      "",
    71  		Period:     "",
    72  		Processors: []processor.Config{},
    73  	}
    74  }
    75  
    76  // IsNoop returns true if this batch policy configuration does nothing.
    77  func (p PolicyConfig) IsNoop() bool {
    78  	if p.ByteSize > 0 {
    79  		return false
    80  	}
    81  	if p.Count > 1 {
    82  		return false
    83  	}
    84  	if !isNoopCondition(p.Condition) {
    85  		return false
    86  	}
    87  	if len(p.Check) > 0 {
    88  		return false
    89  	}
    90  	if len(p.Period) > 0 {
    91  		return false
    92  	}
    93  	if len(p.Processors) > 0 {
    94  		return false
    95  	}
    96  	return true
    97  }
    98  
    99  func (p PolicyConfig) isLimited() bool {
   100  	if p.ByteSize > 0 {
   101  		return true
   102  	}
   103  	if p.Count > 0 {
   104  		return true
   105  	}
   106  	if len(p.Period) > 0 {
   107  		return true
   108  	}
   109  	if !isNoopCondition(p.Condition) {
   110  		return true
   111  	}
   112  	if len(p.Check) > 0 {
   113  		return true
   114  	}
   115  	return false
   116  }
   117  
   118  func (p PolicyConfig) isHardLimited() bool {
   119  	if p.ByteSize > 0 {
   120  		return true
   121  	}
   122  	if p.Count > 0 {
   123  		return true
   124  	}
   125  	if len(p.Period) > 0 {
   126  		return true
   127  	}
   128  	return false
   129  }
   130  
   131  //------------------------------------------------------------------------------
   132  
   133  // Policy implements a batching policy by buffering messages until, based on a
   134  // set of rules, the buffered messages are ready to be sent onwards as a batch.
   135  type Policy struct {
   136  	log log.Modular
   137  
   138  	byteSize  int
   139  	count     int
   140  	period    time.Duration
   141  	cond      condition.Type
   142  	check     *mapping.Executor
   143  	procs     []types.Processor
   144  	sizeTally int
   145  	parts     []types.Part
   146  
   147  	triggered bool
   148  	lastBatch time.Time
   149  
   150  	mSizeBatch   metrics.StatCounter
   151  	mCountBatch  metrics.StatCounter
   152  	mPeriodBatch metrics.StatCounter
   153  	mCheckBatch  metrics.StatCounter
   154  	mCondBatch   metrics.StatCounter
   155  }
   156  
   157  // NewPolicy creates an empty policy with default rules.
   158  func NewPolicy(
   159  	conf PolicyConfig,
   160  	mgr types.Manager,
   161  	log log.Modular,
   162  	stats metrics.Type,
   163  ) (*Policy, error) {
   164  	if !conf.isLimited() {
   165  		return nil, errors.New("batch policy must have at least one active trigger")
   166  	}
   167  	if !conf.isHardLimited() {
   168  		log.Warnln("Batch policy should have at least one of count, period or byte_size set in order to provide a hard batch ceiling.")
   169  	}
   170  	var cond types.Condition
   171  	var err error
   172  	if !isNoopCondition(conf.Condition) {
   173  		cMgr, cLog, cStats := interop.LabelChild("condition", mgr, log, stats)
   174  		if cond, err = condition.New(conf.Condition, cMgr, cLog, cStats); err != nil {
   175  			return nil, fmt.Errorf("failed to create condition: %v", err)
   176  		}
   177  	}
   178  	var check *mapping.Executor
   179  	if len(conf.Check) > 0 {
   180  		if check, err = interop.NewBloblangMapping(mgr, conf.Check); err != nil {
   181  			return nil, fmt.Errorf("failed to parse check: %v", err)
   182  		}
   183  	}
   184  	var period time.Duration
   185  	if len(conf.Period) > 0 {
   186  		if period, err = time.ParseDuration(conf.Period); err != nil {
   187  			return nil, fmt.Errorf("failed to parse duration string: %v", err)
   188  		}
   189  	}
   190  	var procs []types.Processor
   191  	for i, pconf := range conf.Processors {
   192  		pMgr, pLog, pStats := interop.LabelChild(fmt.Sprintf("%v", i), mgr, log, stats)
   193  		proc, err := processor.New(pconf, pMgr, pLog, pStats)
   194  		if err != nil {
   195  			return nil, fmt.Errorf("failed to create processor '%v': %v", i, err)
   196  		}
   197  		procs = append(procs, proc)
   198  	}
   199  	return &Policy{
   200  		log: log,
   201  
   202  		byteSize: conf.ByteSize,
   203  		count:    conf.Count,
   204  		period:   period,
   205  		cond:     cond,
   206  		check:    check,
   207  		procs:    procs,
   208  
   209  		lastBatch: time.Now(),
   210  
   211  		mSizeBatch:   stats.GetCounter("on_size"),
   212  		mCountBatch:  stats.GetCounter("on_count"),
   213  		mPeriodBatch: stats.GetCounter("on_period"),
   214  		mCheckBatch:  stats.GetCounter("on_check"),
   215  		mCondBatch:   stats.GetCounter("on_condition"),
   216  	}, nil
   217  }
   218  
   219  //------------------------------------------------------------------------------
   220  
   221  // Add a new message part to this batch policy. Returns true if this part
   222  // triggers the conditions of the policy.
   223  func (p *Policy) Add(part types.Part) bool {
   224  	p.sizeTally += len(part.Get())
   225  	p.parts = append(p.parts, part)
   226  
   227  	if !p.triggered && p.count > 0 && len(p.parts) >= p.count {
   228  		p.triggered = true
   229  		p.mCountBatch.Incr(1)
   230  		p.log.Traceln("Batching based on count")
   231  	}
   232  	if !p.triggered && p.byteSize > 0 && p.sizeTally >= p.byteSize {
   233  		p.triggered = true
   234  		p.mSizeBatch.Incr(1)
   235  		p.log.Traceln("Batching based on byte_size")
   236  	}
   237  	tmpMsg := message.New(nil)
   238  	tmpMsg.Append(part)
   239  	if p.cond != nil && !p.triggered && p.cond.Check(tmpMsg) {
   240  		p.triggered = true
   241  		p.mCondBatch.Incr(1)
   242  		p.log.Traceln("Batching based on condition")
   243  	}
   244  	tmpMsg.SetAll(p.parts)
   245  	if p.check != nil && !p.triggered {
   246  		test, err := p.check.QueryPart(tmpMsg.Len()-1, tmpMsg)
   247  		if err != nil {
   248  			test = false
   249  			p.log.Errorf("Failed to execute batch check query: %v\n", err)
   250  		}
   251  		if test {
   252  			p.triggered = true
   253  			p.mCheckBatch.Incr(1)
   254  			p.log.Traceln("Batching based on check query")
   255  		}
   256  	}
   257  	return p.triggered || (p.period > 0 && time.Since(p.lastBatch) > p.period)
   258  }
   259  
   260  // Flush clears all messages stored by this batch policy. Returns nil if the
   261  // policy is currently empty.
   262  func (p *Policy) Flush() types.Message {
   263  	var newMsg types.Message
   264  
   265  	resultMsgs := p.FlushAny()
   266  	if len(resultMsgs) == 1 {
   267  		newMsg = resultMsgs[0]
   268  	} else if len(resultMsgs) > 1 {
   269  		newMsg = message.New(nil)
   270  		var parts []types.Part
   271  		for _, m := range resultMsgs {
   272  			m.Iter(func(_ int, p types.Part) error {
   273  				parts = append(parts, p)
   274  				return nil
   275  			})
   276  		}
   277  		newMsg.SetAll(parts)
   278  	}
   279  	return newMsg
   280  }
   281  
   282  // FlushAny clears all messages stored by this batch policy and returns any
   283  // number of discrete message batches. Returns nil if the policy is currently
   284  // empty.
   285  func (p *Policy) FlushAny() []types.Message {
   286  	var newMsg types.Message
   287  	if len(p.parts) > 0 {
   288  		if !p.triggered && p.period > 0 && time.Since(p.lastBatch) > p.period {
   289  			p.mPeriodBatch.Incr(1)
   290  			p.log.Traceln("Batching based on period")
   291  		}
   292  		newMsg = message.New(nil)
   293  		newMsg.Append(p.parts...)
   294  	}
   295  	p.parts = nil
   296  	p.sizeTally = 0
   297  	p.lastBatch = time.Now()
   298  	p.triggered = false
   299  
   300  	if newMsg == nil {
   301  		return nil
   302  	}
   303  
   304  	if len(p.procs) > 0 {
   305  		resultMsgs, res := processor.ExecuteAll(p.procs, newMsg)
   306  		if res != nil {
   307  			if err := res.Error(); err != nil {
   308  				p.log.Errorf("Batch processors resulted in error: %v, the batch has been dropped.", err)
   309  			}
   310  			return nil
   311  		}
   312  		return resultMsgs
   313  	}
   314  
   315  	return []types.Message{newMsg}
   316  }
   317  
   318  // Count returns the number of currently buffered message parts within this
   319  // policy.
   320  func (p *Policy) Count() int {
   321  	return len(p.parts)
   322  }
   323  
   324  // UntilNext returns a duration indicating how long until the current batch
   325  // should be flushed due to a configured period. A negative duration indicates
   326  // a period has not been set.
   327  func (p *Policy) UntilNext() time.Duration {
   328  	if p.period <= 0 {
   329  		return -1
   330  	}
   331  	return time.Until(p.lastBatch.Add(p.period))
   332  }
   333  
   334  //------------------------------------------------------------------------------
   335  
   336  // CloseAsync shuts down the policy resources.
   337  func (p *Policy) CloseAsync() {
   338  	for _, c := range p.procs {
   339  		c.CloseAsync()
   340  	}
   341  }
   342  
   343  // WaitForClose blocks until the processor has closed down.
   344  func (p *Policy) WaitForClose(timeout time.Duration) error {
   345  	stopBy := time.Now().Add(timeout)
   346  	for _, c := range p.procs {
   347  		if err := c.WaitForClose(time.Until(stopBy)); err != nil {
   348  			return err
   349  		}
   350  	}
   351  	return nil
   352  }
   353  
   354  //------------------------------------------------------------------------------