Skip to content
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

Extend the event.Handler API to provide common state info to implementations #2803

Merged
merged 18 commits into from
Nov 22, 2023

Conversation

tpantelis
Copy link
Contributor

See the individual for details. To summarize:

  • Added HandlerState that contains global state common to all event handlers, specifically IsOnGateway and GetRemoteEndpoints which is provided to the handlers via a SetState method.
  • Modified all handlers to use the HandlerState rather than maintaining such state themselves.
  • The event controller now provides synchronization across all event callbacks so handlers don't have to.
  • Added unit tests for handlers that didn't have any prior.

Fixes #2623

@submariner-bot
Copy link
Contributor

🤖 Created branch: z_pr2803/tpantelis/ext_event_hndlr_api
🚀 Full E2E won't run until the "ready-to-test" label is applied. I will add it automatically once the PR has 2 approvals, or you can add it manually.

@tpantelis tpantelis force-pushed the ext_event_hndlr_api branch 2 times, most recently from 3013f75 to 58fbddc Compare November 15, 2023 21:25
@tpantelis tpantelis added the ready-to-test When a PR is ready for full E2E testing label Nov 15, 2023
@@ -43,15 +44,36 @@ type specification struct {
Namespace string
}

type handlerStateType struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this odd on first read — Type suggests this describes a type, but it’s really the anchor for an implementation of HandlerState, so how about handlerStateImpl?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just name it handlerState but it's nice to distinguish it from variables of the same name. I've seen the use of Type in K8s code so I borrowed it (I've used it before as well) - it is actually describing the type. Impl seems Java-ish but I can change it - I'm fine either way (or name it handlerState).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I didn’t bother mentioning handlerState because of the variable name conflict! I prefer Impl personally, especially as more type parameters start appearing in various places. The naming in Kubernetes seems to be shifting too, at least in discussions I’ve been having recently people talk about Impl, and it’s about 50/50 now.

if err := c.handlers.LocalEndpointCreated(endpoint); err != nil {
return err //nolint:wrapcheck // Let the caller wrap it
if endpoint.Spec.Hostname == c.hostname {
c.handlerState.isOnGateway.Store(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reads go through IsOnGateway(), so it feels unbalanced to directly access the atomic here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isOnGateway is private so it really doesn't matter either way. But to balance it we can either add a private function to wrap isOnGateway.Store or not use IsOnGateway() internally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it’s not important, but a private function would be good (not exposing the atomic for reads — I’d rather keep this encapsulated so we can change the implementation easily).

type Handler interface {
// Init is called once on startup to let the handler initialize any state it needs.
Init() error

// SetHandlerState is called once on startup after Init with the HandlerState that can be used to access global data from event callbacks.
SetState(handlerCtx HandlerState)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a context, or a state?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the amount of churn involved, is there a reason to have a separate SetState method instead of adding the handler state to Init?

Currently the handlers are set up when the registry is constructed, and then controller construction sets the shared handler state. But AFAICT nothing would stop a handler being added after that, and then it wouldn’t be given the shared state...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a context, or a state?

I went back and forth on that. I finally went with state b/c context is usually used per function and this state persists.

I thought about passing it via Init but that is called before the controller is initialized and it's the controller that owns/implements the state. The registry and controller are completely decoupled. But we could have the registry construct and start the controller - we'd have to have a public method on the controller to get the HandlerState but that seemed awkward, unless we have the controller called Init. Also, each Handler impl would have to explicitly store the state, presumably call SetState on the HandlerBase, but this seemed awkward to me. That's why I went with this approach.

But AFAICT nothing would stop a handler being added after that, and then it wouldn’t be given the shared state...

You're right but that would problematic b/c eventHandlers in Registry is not synchronized. The Registry is designed to add all the handlers first before starting the controller. We could force this by passing all the handlers via NewRegistry and removing AddHandlers. I can do that in a followup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thanks for the explanation, that makes sense. I like the idea of dropping AddHandlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of dropping AddHandlers

57cdf7e

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SetState(handlerCtx HandlerState)
SetState(handlerState HandlerState)

if you respin this.

Comment on lines 66 to 68
vtepIP, kp.State().IsOnGateway())

if kp.isGatewayNode {
if kp.State().IsOnGateway() {
Copy link
Member

@skitt skitt Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggests a small (pre-existing) race here, but it’s protected by the lock. However to make the correctness more obvious it would be better to store the current return value of IsOnGateway() and use that both for logging and decision-marking (so it’s apparent that the branch taken matches the log message).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, for readability.

The controller was using the syncMutex to protect access to the isGatewayNode
field, although not really necessary since the callbacks emanate from the
same syncer on the same thread.

In addition, some handlers employ their own mutex across multiple callbacks,
although unnecessary in most cases. However a hidden caveat with the handler
API is that while the Endpoint-related events (ie LocalEndpointCreated,
TransitionToGateway etc) are single-threaded wrt one another and don't
need synchronization, the Mode-related events emanate from a separate
syncer and thread. So if data is accessed across Endpoint and Node
events then synchronization is needed. Rather then having each handler
implementation be aware of this behavior, it is simpler and safer to
have the event controller use the syncMutex to synchronization access
to all the callbacks and remove the synchronization in the handlers.

Signed-off-by: Tom Pantelis <[email protected]>
This will enable event handlers to query common global state from event
callback methods. This will replace the state currently being maintained
by many handlers. The state instance is set once for each handler
instead of passed to each event callback method to enable the state to be
accessed outside of the event callbacks. The HandlerBase implements
SetState.

Signed-off-by: Tom Pantelis <[email protected]>
...for reuse in other event handler tests.

Signed-off-by: Tom Pantelis <[email protected]>
Also added unit tests.

Signed-off-by: Tom Pantelis <[email protected]>
Also added unit tests.

Signed-off-by: Tom Pantelis <[email protected]>
Also added unit tests.

Signed-off-by: Tom Pantelis <[email protected]>
This enables unit tests to mock the behavior.

Signed-off-by: Tom Pantelis <[email protected]>
Define a function so that unit tests can provide a fake implementation.
Also consolidate the NewHandler parameters into a HandlerConfig struct.

Signed-off-by: Tom Pantelis <[email protected]>
...as AwaitRoutes checks for Dst IP. Also rename AwaitRoutes to AwaitDstRoutes.

Signed-off-by: Tom Pantelis <[email protected]>
...instead of hard-coded strings.

Signed-off-by: Tom Pantelis <[email protected]>
Signed-off-by: Tom Pantelis <[email protected]>
The mutex was kept to provide atomicity when updating the gateway and
host network dataplane since these are also invoked via the route config
syncer which runs on its own thread. The synchronization may actually not
be necessary but it's better to be safe and the overhead in negligible.

Signed-off-by: Tom Pantelis <[email protected]>
This ensures all handlers are registered before starting the controller.

Signed-off-by: Tom Pantelis <[email protected]>
type Handler interface {
// Init is called once on startup to let the handler initialize any state it needs.
Init() error

// SetHandlerState is called once on startup after Init with the HandlerState that can be used to access global data from event callbacks.
SetState(handlerCtx HandlerState)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SetState(handlerCtx HandlerState)
SetState(handlerState HandlerState)

if you respin this.

"github.com/submariner-io/admiral/pkg/slices"
)

type OVSDBClient struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing OVSDB test code, that takes us back!

@skitt skitt merged commit fd07457 into submariner-io:devel Nov 22, 2023
35 of 36 checks passed
@submariner-bot
Copy link
Contributor

🤖 Closed branches: [z_pr2803/tpantelis/ext_event_hndlr_api]

@tpantelis tpantelis deleted the ext_event_hndlr_api branch December 8, 2023 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready-to-test When a PR is ready for full E2E testing
Projects
No open projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Extend the event.Handler API to provide common contextual info to implementations
4 participants