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