-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add: Error Handling Environment Bundle to Bento configuration (#174)
- Loading branch information
1 parent
86e8d2b
commit cfeec19
Showing
11 changed files
with
407 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package strict | ||
|
||
import ( | ||
"github.com/warpstreamlabs/bento/internal/bundle" | ||
"github.com/warpstreamlabs/bento/internal/component/processor" | ||
) | ||
|
||
// StrictBundle modifies a provided bundle environment so that all procesors | ||
// will fail an entire batch if any any message-level error is encountered. These | ||
// failed batches are nacked and/or reprocessed depending on your input. | ||
func StrictBundle(b *bundle.Environment) *bundle.Environment { | ||
strictEnv := b.Clone() | ||
|
||
for _, spec := range b.ProcessorDocs() { | ||
_ = strictEnv.ProcessorAdd(func(conf processor.Config, nm bundle.NewManagement) (processor.V1, error) { | ||
proc, err := b.ProcessorInit(conf, nm) | ||
if err != nil { | ||
return nil, err | ||
} | ||
proc = wrapWithStrict(proc) | ||
return proc, err | ||
}, spec) | ||
} | ||
|
||
// TODO: Overwrite inputs for retry with backoff | ||
|
||
return strictEnv | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package strict_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/warpstreamlabs/bento/internal/bundle" | ||
"github.com/warpstreamlabs/bento/internal/bundle/strict" | ||
"github.com/warpstreamlabs/bento/internal/component/testutil" | ||
"github.com/warpstreamlabs/bento/internal/manager" | ||
"github.com/warpstreamlabs/bento/internal/message" | ||
|
||
_ "github.com/warpstreamlabs/bento/internal/impl/pure" | ||
) | ||
|
||
func TestStrictBundleProcessor(t *testing.T) { | ||
senv := strict.StrictBundle(bundle.GlobalEnvironment) | ||
tCtx := context.Background() | ||
|
||
pConf, err := testutil.ProcessorFromYAML(` | ||
bloblang: root = this | ||
`) | ||
require.NoError(t, err) | ||
|
||
mgr, err := manager.New( | ||
manager.ResourceConfig{}, | ||
manager.OptSetEnvironment(senv), | ||
) | ||
require.NoError(t, err) | ||
|
||
proc, err := mgr.NewProcessor(pConf) | ||
require.NoError(t, err) | ||
|
||
msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) | ||
msgs, res := proc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.ErrorContains(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
msg = message.QuickBatch([][]byte{[]byte(`{"hello":"world"}`)}) | ||
msgs, res = proc.ProcessBatch(tCtx, msg) | ||
require.NoError(t, res) | ||
require.Len(t, msgs, 1) | ||
assert.Equal(t, 1, msgs[0].Len()) | ||
assert.Equal(t, `{"hello":"world"}`, string(msgs[0].Get(0).AsBytes())) | ||
} | ||
|
||
func TestStrictBundleProcessorMultiMessage(t *testing.T) { | ||
senv := strict.StrictBundle(bundle.GlobalEnvironment) | ||
tCtx := context.Background() | ||
|
||
pConf, err := testutil.ProcessorFromYAML(` | ||
bloblang: root = this | ||
`) | ||
require.NoError(t, err) | ||
|
||
mgr, err := manager.New( | ||
manager.ResourceConfig{}, | ||
manager.OptSetEnvironment(senv), | ||
) | ||
require.NoError(t, err) | ||
|
||
proc, err := mgr.NewProcessor(pConf) | ||
require.NoError(t, err) | ||
|
||
msg := message.QuickBatch([][]byte{ | ||
[]byte("not a structured doc"), | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte(`{"bar":"rab"}`), | ||
}) | ||
msgs, res := proc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.ErrorContains(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
// Ensure the ordering of the message does not influence the error message | ||
msg = message.QuickBatch([][]byte{ | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte("not a structured doc"), | ||
[]byte(`{"bar":"rab"}`), | ||
}) | ||
msgs, res = proc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.ErrorContains(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
// Multiple errored messages | ||
msg = message.QuickBatch([][]byte{ | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte("not a structured doc"), | ||
[]byte(`another unstructred doc`), | ||
}) | ||
msgs, res = proc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.ErrorContains(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package strict | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/warpstreamlabs/bento/internal/batch" | ||
iprocessor "github.com/warpstreamlabs/bento/internal/component/processor" | ||
"github.com/warpstreamlabs/bento/internal/message" | ||
) | ||
|
||
func wrapWithStrict(p iprocessor.V1) iprocessor.V1 { | ||
t := &strictProcessor{ | ||
wrapped: p, | ||
enabled: true, | ||
} | ||
return t | ||
} | ||
|
||
//------------------------------------------------------------------------------ | ||
|
||
// strictProcessor fails batch processing if any message contains an error. | ||
type strictProcessor struct { | ||
wrapped iprocessor.V1 | ||
enabled bool | ||
} | ||
|
||
func (s *strictProcessor) ProcessBatch(ctx context.Context, b message.Batch) ([]message.Batch, error) { | ||
if !s.enabled { | ||
return s.wrapped.ProcessBatch(ctx, b) | ||
} | ||
|
||
batches, err := s.wrapped.ProcessBatch(ctx, b) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Iterate through all messages and populate a batch.Error type, calling Failed() | ||
// for each errored message. Otherwise, every message in the batch is treated as a failure. | ||
for _, msg := range batches { | ||
var batchErr *batch.Error | ||
_ = msg.Iter(func(i int, p *message.Part) error { | ||
mErr := p.ErrorGet() | ||
if mErr == nil { | ||
return nil | ||
} | ||
if batchErr == nil { | ||
batchErr = batch.NewError(msg, mErr) | ||
} | ||
batchErr.Failed(i, mErr) | ||
return nil | ||
}) | ||
if batchErr != nil { | ||
return nil, batchErr | ||
} | ||
} | ||
|
||
return batches, nil | ||
} | ||
|
||
func (s *strictProcessor) Close(ctx context.Context) error { | ||
return s.wrapped.Close(ctx) | ||
} | ||
|
||
func (s *strictProcessor) UnwrapProc() iprocessor.V1 { | ||
return s.wrapped | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package strict | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/warpstreamlabs/bento/internal/message" | ||
) | ||
|
||
//------------------------------------------------------------------------------ | ||
|
||
type mockProc struct{} | ||
|
||
func (m mockProc) ProcessBatch(ctx context.Context, msg message.Batch) ([]message.Batch, error) { | ||
for _, m := range msg { | ||
_, err := m.AsStructuredMut() | ||
m.ErrorSet(err) | ||
} | ||
return []message.Batch{msg}, nil | ||
} | ||
|
||
func (m mockProc) Close(ctx context.Context) error { | ||
// Do nothing as our processor doesn't require resource cleanup. | ||
return nil | ||
} | ||
|
||
//------------------------------------------------------------------------------ | ||
|
||
func TestProcessorWrapWithStrict(t *testing.T) { | ||
tCtx := context.Background() | ||
|
||
// Wrap the processor with the strict interface | ||
strictProc := wrapWithStrict(mockProc{}) | ||
|
||
msg := message.QuickBatch([][]byte{[]byte("not a structured doc")}) | ||
msgs, res := strictProc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.EqualError(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
msg = message.QuickBatch([][]byte{[]byte(`{"hello":"world"}`)}) | ||
msgs, res = strictProc.ProcessBatch(tCtx, msg) | ||
require.NoError(t, res) | ||
require.Len(t, msgs, 1) | ||
assert.Equal(t, 1, msgs[0].Len()) | ||
assert.Equal(t, `{"hello":"world"}`, string(msgs[0].Get(0).AsBytes())) | ||
} | ||
|
||
func TestProcessorWrapWithStrictMultiMessage(t *testing.T) { | ||
tCtx := context.Background() | ||
|
||
// Wrap the processor with the strict interface | ||
strictProc := wrapWithStrict(mockProc{}) | ||
|
||
msg := message.QuickBatch([][]byte{ | ||
[]byte("not a structured doc"), | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte(`{"bar":"rab"}`), | ||
}) | ||
msgs, res := strictProc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.Error(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
// Ensure the ordering of the message does not influence the error message | ||
msg = message.QuickBatch([][]byte{ | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte("not a structured doc"), | ||
[]byte(`{"bar":"rab"}`), | ||
}) | ||
msgs, res = strictProc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.Error(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
|
||
// Multiple errored messages | ||
msg = message.QuickBatch([][]byte{ | ||
[]byte(`{"foo":"oof"}`), | ||
[]byte("not a structured doc"), | ||
[]byte(`another unstructred doc`), | ||
}) | ||
msgs, res = strictProc.ProcessBatch(tCtx, msg) | ||
require.Empty(t, msgs) | ||
require.Error(t, res) | ||
assert.Error(t, res, "invalid character 'o' in literal null (expecting 'u')") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.