Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a2d6098
feat: make executor event handling timeout configurable
sachin9058 Apr 4, 2026
bafd7a1
Add VEX / ignore mitigations for CVE-2026-34040 (#6243)
evankanderson Apr 2, 2026
adcaff8
feat(inventory): add evaluation_outputs table for persisting structur…
dakshhhhh16 Apr 2, 2026
327e7a3
test(projects/features): add unit tests for feature flag helpers (#6246)
dakshhhhh16 Apr 3, 2026
d3da8bf
build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 (…
dependabot[bot] Apr 3, 2026
15daf07
Resolve flaky TestGetGrpcConnection and upgrade dependencies (#6264)
krrish175-byte Apr 3, 2026
6abe7ac
build(deps): bump the tools group in /tools with 4 updates (#6230)
dependabot[bot] Apr 3, 2026
6f131a0
Add pre-validation for repo register command to prevent unnecessary a…
sachin9058 Apr 3, 2026
fddf2aa
Move internal/engine/errors to pkg/engine/errors to reduce coupling (…
sachin9058 Apr 3, 2026
94f8c11
Remove git_pr_diffs flag (#6258)
DharunMR Apr 3, 2026
fcf4ad6
build(deps): bump lodash from 4.17.23 to 4.18.1 in /docs (#6260)
dependabot[bot] Apr 3, 2026
cd51b27
Auto-generated cli documentation update - 2026-04-03 12:06:15 (#6267)
github-actions[bot] Apr 3, 2026
ae91fc2
fix(metrics): swap incorrectly assigned entity and profile duration m…
theycallmeaabie Apr 4, 2026
0678394
removed dependency_extract feature flag (#6272)
DharunMR Apr 4, 2026
064834f
Decouple pkg/engine/errors from internal/db using adapter layer (#6266)
sachin9058 Apr 4, 2026
1d989bd
feat: expose executor timeout configuration and add validation, loggi…
sachin9058 Apr 5, 2026
817bbd3
test: disable parallel execution for profile DB test to avoid unique …
sachin9058 Apr 5, 2026
38b63a1
lint-skipped-for-parallel-tests
sachin9058 Apr 5, 2026
4773bba
avoid enforced timeout at handler level
sachin9058 Apr 5, 2026
d662ebb
Make execution timeout configurable and improve test parallelization
sachin9058 Apr 6, 2026
14e4130
Move internal/engine/errors to pkg/engine/errors to reduce coupling (…
sachin9058 Apr 3, 2026
74c52bf
Remove git_pr_diffs flag (#6258)
DharunMR Apr 3, 2026
9572bdd
removed dependency_extract feature flag (#6272)
DharunMR Apr 4, 2026
3351066
Decouple pkg/engine/errors from internal/db using adapter layer (#6266)
sachin9058 Apr 4, 2026
b8d4449
Test(actions): add unit tests for rule actions (#6208)
dakshhhhh16 Apr 6, 2026
96b7e64
Additional info delete ruletype (#6273)
DharunMR Apr 6, 2026
9ecafed
Fix rebase issues, resolve conflicts, and sync validator with upstream
sachin9058 Apr 6, 2026
ae7c773
Merge branch 'main' into feat-configurable-executor-timeout-clean
sachin9058 Apr 6, 2026
49b3bb8
fix: add missing comments for exported handler methods
sachin9058 Apr 6, 2026
3d12532
Merge branch 'main' into feat-configurable-executor-timeout-clean
sachin9058 Apr 6, 2026
f080750
Merge branch 'main' into feat-configurable-executor-timeout-clean
sachin9058 Apr 7, 2026
93492eb
Merge branch 'main' into feat-configurable-executor-timeout-clean
sachin9058 Apr 14, 2026
68098a4
Make executor timeout configurable
sachin9058 Apr 20, 2026
45abf96
Merge branch 'main' into feat-configurable-executor-timeout-clean
sachin9058 Apr 20, 2026
7277590
Cover default timeout fallback
sachin9058 Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/db/profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,8 @@ func TestProfileLabels(t *testing.T) {
}
}

//nolint:paralleltest,tparallel // top-level parallel causes interference with other tests (gomock/shared state)
func TestCreateProfileStatusSingleRuleTransitions(t *testing.T) {
t.Parallel()
Comment thread
sachin9058 marked this conversation as resolved.

tests := []struct {
name string
Expand Down
71 changes: 46 additions & 25 deletions internal/engine/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,39 +25,53 @@ const (
// DefaultExecutionTimeout is the timeout for execution of a set
// of profiles on an entity.
DefaultExecutionTimeout = 5 * time.Minute

// ArtifactSignatureWaitPeriod is the waiting period for potential artifact signature to be available
// before proceeding with evaluation.
ArtifactSignatureWaitPeriod = 10 * time.Second
)

// ExecutorEventHandler is responsible for consuming entity events, passing
// entities to the executor, and then publishing the results.
// ExecutorEventHandler handles entity events, executes evaluations,
// and publishes the results.
type ExecutorEventHandler struct {
evt interfaces.Publisher
handlerMiddleware []message.HandlerMiddleware
wgEntityEventExecution *sync.WaitGroup
executor Executor
// cancels are a set of cancel functions for current entity events in flight.
// This allows us to cancel rule evaluation directly when terminationContext
// is cancelled.
Comment thread
sachin9058 marked this conversation as resolved.

executionTimeout time.Duration

cancels []*context.CancelFunc
lock sync.Mutex
closed bool
}

// NewExecutorEventHandler creates the event handler for the executor
// NewExecutorEventHandler creates a new ExecutorEventHandler with
// configurable execution timeout.
func NewExecutorEventHandler(
ctx context.Context,
evt interfaces.Publisher,
handlerMiddleware []message.HandlerMiddleware,
executor Executor,
executionTimeout time.Duration,
) *ExecutorEventHandler {

if executionTimeout <= 0 {
executionTimeout = DefaultExecutionTimeout
}

eh := &ExecutorEventHandler{
evt: evt,
wgEntityEventExecution: &sync.WaitGroup{},
handlerMiddleware: handlerMiddleware,
executor: executor,
executionTimeout: executionTimeout,
}

zerolog.Ctx(ctx).Debug().
Dur("execution_timeout", executionTimeout).
Msg("executor event handler initialized")

go func() {
<-ctx.Done()
eh.lock.Lock()
Expand All @@ -73,30 +87,31 @@ func NewExecutorEventHandler(
return eh
}

// Register implements the Consumer interface.
// Register registers the handler for entity evaluation events.
func (e *ExecutorEventHandler) Register(r interfaces.Registrar) {
r.Register(constants.TopicQueueEntityEvaluate, e.HandleEntityEvent, e.handlerMiddleware...)
}

// Wait waits for all the entity executions to finish.
// Wait blocks until all entity event executions are complete.
func (e *ExecutorEventHandler) Wait() {
e.wgEntityEventExecution.Wait()
}

// HandleEntityEvent handles events coming from webhooks/signals
// as well as the init event.
// HandleEntityEvent processes incoming entity events and triggers evaluation.
func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {

// NOTE: we're _deliberately_ "escaping" from the parent context's Cancel/Done
// completion, because the default watermill behavior for both Go channels and
// SQL is to process messages sequentially, but we need additional parallelism
// beyond that. When we switch to a different message processing system, we
// beyond that. When we switch to a different message processing system, we
// should aim to remove this goroutine altogether and have the messaging system
// provide the parallelism.
// We _do_ still want to cancel on shutdown, however.
// TODO: Make this timeout configurable
msgCtx := context.WithoutCancel(msg.Context())
//nolint:gosec // this is called when we iterate over e.cancels

// This allows us to cancel rule evaluation directly when terminationContext
// is cancelled.
//nolint:gosec
msgCtx, shutdownCancel := context.WithCancel(msgCtx)

e.lock.Lock()
Expand All @@ -108,7 +123,8 @@ func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {
e.cancels = append(e.cancels, &shutdownCancel)
e.lock.Unlock()

// Let's not share memory with the caller. Note that this does not copy Context
// Copy the message so the async goroutine does not share mutable state with
// the original Watermill message.
msg = msg.Copy()

inf, err := entities.ParseEntityEvent(msg)
Expand All @@ -117,8 +133,10 @@ func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {
}

e.wgEntityEventExecution.Add(1)

go func() {
defer e.wgEntityEventExecution.Done()

if inf.Type == pb.Entity_ENTITY_ARTIFACTS {
// Wait for artifact signatures, but allow early exit on shutdown
select {
Expand All @@ -128,8 +146,9 @@ func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {
}
}

ctx, cancel := context.WithTimeout(msgCtx, DefaultExecutionTimeout)
ctx, cancel := context.WithTimeout(msgCtx, e.executionTimeout)
defer cancel()

defer func() {
e.lock.Lock()
e.cancels = slices.DeleteFunc(e.cancels, func(cf *context.CancelFunc) bool {
Expand All @@ -140,30 +159,34 @@ func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {

ctx = engcontext.WithEntityContext(ctx, &engcontext.EntityContext{
Project: engcontext.Project{ID: inf.ProjectID},
// TODO: extract Provider name from ProviderID?
Provider: engcontext.Provider{
Name: inf.ProviderID.String(),
},
})

ts := minderlogger.BusinessRecord(ctx)
ctx = ts.WithTelemetry(ctx)

logger := zerolog.Ctx(ctx)

if err := inf.WithExecutionIDFromMessage(msg); err != nil {
logger.Info().
logger.Debug().
Comment thread
sachin9058 marked this conversation as resolved.
Str("message_id", msg.UUID).
Msg("message does not contain execution ID, skipping")
return
}

err := e.executor.EvalEntityEvent(ctx, inf)

// record telemetry regardless of error. We explicitly record telemetry
// here even though we also record it in the middleware because the evaluation
// is done in a separate goroutine which usually still runs after the middleware
// had already recorded the telemetry.
Comment thread
sachin9058 marked this conversation as resolved.
logMsg := logger.Info()
if err != nil {
logMsg = logger.Error()
}

// record telemetry regardless of error. We explicitly record telemetry
// here even though we also record it in the middleware because the evaluation
// is done in a separate goroutine which usually still runs after the middleware
// had already recorded the telemetry.
Comment thread
evankanderson marked this conversation as resolved.
ts.Record(logMsg).Send()

if err != nil {
Expand All @@ -172,18 +195,16 @@ func (e *ExecutorEventHandler) HandleEntityEvent(msg *message.Message) error {
Str("provider_id", inf.ProviderID.String()).
Str("entity", inf.Type.String()).
Str("entity_id", inf.EntityID.String()).
Err(err).Msg("got error while evaluating entity event")
Err(err).
Msg("got error while evaluating entity event")
}

// We don't need to unset the execution ID because the event is going to be
// deleted from the database anyway. The aggregator will take care of that.
msg, err := inf.BuildMessage()
if err != nil {
logger.Err(err).Msg("error building message")
return
}

// Publish the result of the entity evaluation
if err := e.evt.Publish(constants.TopicQueueEntityFlush, msg); err != nil {
logger.Err(err).Msg("error publishing flush event")
}
Expand Down
Loading
Loading