-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate events when nodes are approved/rejected #3772
base: main
Are you sure you want to change the base?
Changes from all commits
0632042
9eddaf5
13a3611
4529b4b
85cbacc
4ad5289
256ef60
836ba77
3b5ffbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package manager | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/bacalhau-project/bacalhau/pkg/jobstore" | ||
"github.com/bacalhau-project/bacalhau/pkg/models" | ||
"github.com/bacalhau-project/bacalhau/pkg/orchestrator" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type NodeEventListener struct { | ||
broker orchestrator.EvaluationBroker | ||
jobstore jobstore.Store | ||
} | ||
|
||
func NewNodeEventListener(broker orchestrator.EvaluationBroker, jobstore jobstore.Store) *NodeEventListener { | ||
return &NodeEventListener{ | ||
broker: broker, | ||
jobstore: jobstore, | ||
} | ||
} | ||
|
||
// HandleNodeEvent will receive events from the node manager, and is responsible for deciding what | ||
// to do in response to those events. This NodeEventHandler implementation is expected to | ||
// create new evaluations based on the events received. | ||
func (n *NodeEventListener) HandleNodeEvent(ctx context.Context, info models.NodeInfo, evt NodeEvent) { | ||
log.Ctx(ctx).Info().Msgf("Received node event %s for node %s", evt.String(), info.NodeID) | ||
} | ||
|
||
var _ NodeEventHandler = &NodeEventListener{} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
//go:generate mockgen --source events.go --destination mocks.go --package manager | ||
package manager | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
"github.com/benbjohnson/clock" | ||
|
||
"github.com/bacalhau-project/bacalhau/pkg/models" | ||
) | ||
|
||
// NodeEvent represents the type of event that can be emitted by the NodeEventEmitter. | ||
type NodeEvent int | ||
|
||
const ( | ||
NodeEventApproved NodeEvent = iota | ||
NodeEventRejected | ||
NodeEventDeleted | ||
NodeEventConnected | ||
NodeEventDisconnected | ||
) | ||
|
||
func (n NodeEvent) String() string { | ||
if n == NodeEventApproved { | ||
return "NodeEventApproved" | ||
} else if n == NodeEventRejected { | ||
return "NodeEventRejected" | ||
} else if n == NodeEventDeleted { | ||
return "NodeEventDeleted" | ||
} else if n == NodeEventConnected { | ||
return "NodeEventConnected" | ||
} else if n == NodeEventDisconnected { | ||
return "NodeEventDisconnected" | ||
} | ||
|
||
return "UnknownNodeEvent" | ||
} | ||
|
||
type NodeEventEmitterOption func(emitter *NodeEventEmitter) | ||
|
||
// WithClock is an option that can be used to set the clock for the NodeEventEmitter. This is useful | ||
// for testing purposes. | ||
func WithClock(clock clock.Clock) NodeEventEmitterOption { | ||
return func(emitter *NodeEventEmitter) { | ||
emitter.clock = clock | ||
} | ||
} | ||
Comment on lines
+44
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this is used anywhere, was it meant to be used in a test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was, and is with b9a6953 |
||
|
||
// NodeEventHandler defines the interface for components which wish to respond to node events | ||
type NodeEventHandler interface { | ||
HandleNodeEvent(ctx context.Context, info models.NodeInfo, event NodeEvent) | ||
} | ||
|
||
// NodeEventEmitter is a struct that will be used to emit events and register callbacks for those events. | ||
// Events will be emitted by the node manager when a node is approved or rejected, and the expectation | ||
// is that they will be consumed by the evaluation broker to create new evaluations. | ||
// It is safe for concurrent use. | ||
type NodeEventEmitter struct { | ||
mu sync.Mutex | ||
callbacks []NodeEventHandler | ||
clock clock.Clock | ||
emitTimeout time.Duration | ||
} | ||
|
||
func NewNodeEventEmitter(options ...NodeEventEmitterOption) *NodeEventEmitter { | ||
emitter := &NodeEventEmitter{ | ||
callbacks: make([]NodeEventHandler, 0), | ||
clock: clock.New(), | ||
emitTimeout: 1 * time.Second, | ||
} | ||
|
||
for _, option := range options { | ||
option(emitter) | ||
} | ||
|
||
return emitter | ||
} | ||
|
||
// RegisterCallback will register a callback for a specific event and add it to the list | ||
// of existing callbacks, all of which will be called then that event is emitted. | ||
func (e *NodeEventEmitter) RegisterHandler(callback NodeEventHandler) { | ||
e.mu.Lock() | ||
defer e.mu.Unlock() | ||
|
||
e.callbacks = append(e.callbacks, callback) | ||
} | ||
|
||
// EmitEvent will emit an event and call all the callbacks registered for that event. These callbacks | ||
// are called in a goroutine and are expected to complete quickly. | ||
func (e *NodeEventEmitter) EmitEvent(ctx context.Context, info models.NodeInfo, event NodeEvent) error { | ||
e.mu.Lock() | ||
defer e.mu.Unlock() | ||
|
||
completedChan := make(chan struct{}) | ||
wg := sync.WaitGroup{} | ||
|
||
newCtx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
|
||
for _, hlr := range e.callbacks { | ||
wg.Add(1) | ||
go func(handler NodeEventHandler, ctx context.Context) { | ||
defer wg.Done() | ||
|
||
handler.HandleNodeEvent(ctx, info, event) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am thinking it's worth passing a sub-context with a timeout to these methods instead of the parent context. This way we ensure that all the handlers receive the cancellation signal and can stop their execution, and (try to) prevent goroutine leaks on timeout. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, will give it a try. The problem might be that the timeout will not be using the mock clock, and so is only really useful for cleaning up unfinished callbacks rather than being useful to replace the timeout on each callback. Cancellable version in 1da8eb2 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
oo yeah, good point. Change looks good here - thanks. |
||
}(hlr, newCtx) | ||
} | ||
|
||
// Wait for the waitgroup and then close the channel to signal completion. This allows | ||
// us to select on the completed channel as well as the timeout | ||
go func() { | ||
defer close(completedChan) | ||
wg.Wait() | ||
}() | ||
|
||
select { | ||
case <-completedChan: | ||
return nil | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-e.clock.After(e.emitTimeout): | ||
return fmt.Errorf("timed out waiting for %s event callbacks to complete", event.String()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
//go:build unit || !integration | ||
|
||
package manager_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/benbjohnson/clock" | ||
gomock "go.uber.org/mock/gomock" | ||
|
||
"github.com/bacalhau-project/bacalhau/pkg/models" | ||
"github.com/bacalhau-project/bacalhau/pkg/node/manager" | ||
"github.com/stretchr/testify/suite" | ||
) | ||
|
||
type EventEmitterSuite struct { | ||
suite.Suite | ||
ctrl *gomock.Controller | ||
ctx context.Context | ||
clock *clock.Mock | ||
} | ||
|
||
func TestEventEmitterSuite(t *testing.T) { | ||
suite.Run(t, new(EventEmitterSuite)) | ||
} | ||
|
||
func (s *EventEmitterSuite) SetupTest() { | ||
s.ctrl = gomock.NewController(s.T()) | ||
s.ctx = context.Background() | ||
s.clock = clock.NewMock() | ||
} | ||
|
||
func (s *EventEmitterSuite) TestNewNodeEventEmitter() { | ||
e := manager.NewNodeEventEmitter() | ||
s.NotNil(e) | ||
|
||
mockHandler := manager.NewMockNodeEventHandler(s.ctrl) | ||
mockHandler.EXPECT().HandleNodeEvent(gomock.Any(), gomock.Any(), manager.NodeEventApproved) | ||
|
||
e.RegisterHandler(mockHandler) | ||
|
||
err := e.EmitEvent(s.ctx, models.NodeInfo{}, manager.NodeEventApproved) | ||
s.NoError(err) | ||
} | ||
|
||
func (s *EventEmitterSuite) TestRegisterCallback() { | ||
e := manager.NewNodeEventEmitter() | ||
s.NotNil(e) | ||
|
||
mockHandler := manager.NewMockNodeEventHandler(s.ctrl) | ||
e.RegisterHandler(mockHandler) | ||
} | ||
|
||
func (s *EventEmitterSuite) TestEmitEvent() { | ||
e := manager.NewNodeEventEmitter() | ||
s.NotNil(e) | ||
|
||
mockHandler := manager.NewMockNodeEventHandler(s.ctrl) | ||
mockHandler.EXPECT().HandleNodeEvent(gomock.Any(), gomock.Any(), manager.NodeEventApproved) | ||
mockHandler.EXPECT().HandleNodeEvent(gomock.Any(), gomock.Any(), manager.NodeEventRejected) | ||
|
||
e.RegisterHandler(mockHandler) | ||
|
||
err := e.EmitEvent(s.ctx, models.NodeInfo{}, manager.NodeEventApproved) | ||
s.NoError(err) | ||
|
||
err = e.EmitEvent(s.ctx, models.NodeInfo{}, manager.NodeEventRejected) | ||
s.NoError(err) | ||
} | ||
|
||
func (s *EventEmitterSuite) TestEmitEventWithNoCallbacks() { | ||
e := manager.NewNodeEventEmitter() | ||
s.NotNil(e) | ||
|
||
err := e.EmitEvent(s.ctx, models.NodeInfo{}, manager.NodeEventApproved) | ||
s.NoError(err) | ||
} | ||
|
||
func (s *EventEmitterSuite) TestEmitWithSlowCallback() { | ||
e := manager.NewNodeEventEmitter(manager.WithClock(s.clock)) | ||
s.NotNil(e) | ||
|
||
e.RegisterHandler(testSleepyHandler{s.clock}) | ||
|
||
go func() { | ||
s.clock.Add(10 * time.Second) | ||
}() | ||
Comment on lines
+87
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this need a go routine? I'd assume adding 10 seconds to the clock is non-blocking? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some weirdness with the mock clock where it won't work when it's inline. I've been trying to work out if I'm just holding it wrong but I suspect it's something to do with the work in EmitEvent happening in a goroutine and requiring a yield (but I'm not 100% sure) |
||
|
||
err := e.EmitEvent(s.ctx, models.NodeInfo{}, manager.NodeEventRejected) | ||
s.Error(err) | ||
} | ||
|
||
type testSleepyHandler struct { | ||
c *clock.Mock | ||
} | ||
|
||
func (t testSleepyHandler) HandleNodeEvent(ctx context.Context, info models.NodeInfo, event manager.NodeEvent) { | ||
t.c.Sleep(2 * time.Second) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CONNECTED
now implies healthy I assume? or was this just never used?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was never used and I think only worked because the default for ints is 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct "healthy" implied something that we don't know about the node. "Connected" is the right term (for now)