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

refactor: Use events to test network logic #2700

Merged
merged 42 commits into from
Jun 20, 2024

Conversation

nasdf
Copy link
Member

@nasdf nasdf commented Jun 7, 2024

Relevant issue(s)

Resolves #2699
Resolves #2687

Description

This PR refactors the events package and updates the networking tests to use events.

I think there is a bit more clean up to be done, but the majority should be ready for review.

Tasks

  • I made sure the code is well commented, particularly hard-to-understand areas.
  • I made sure the repository-held documentation is changed accordingly.
  • I made sure the pull request title adheres to the conventional commit style (the subset used in the project can be found in tools/configs/chglog/config.yml).
  • I made sure to discuss its limitations such as threats to validity, vulnerability to mistake and misuse, robustness to invalidation of assumptions, resource requirements, ...

How has this been tested?

make test

Specify the platform(s) on which this was tested:

  • MacOS

@nasdf nasdf added refactor This issue specific to or requires *notable* refactoring of existing codebases and components area/network Related to internal/external network components labels Jun 7, 2024
@nasdf nasdf added this to the DefraDB v0.12 milestone Jun 7, 2024
@nasdf nasdf self-assigned this Jun 7, 2024
@nasdf nasdf requested a review from a team June 7, 2024 20:28
events/bus.go Outdated Show resolved Hide resolved
events/bus.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@fredcarle fredcarle left a comment

Choose a reason for hiding this comment

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

I need to head out but first pass looks really good. I love that you removed 1000 lines of code 🤘. I'll finish a more in depth review on Monday.

Copy link

codecov bot commented Jun 7, 2024

Codecov Report

Attention: Patch coverage is 90.62500% with 15 lines in your changes missing coverage. Please review.

Project coverage is 78.11%. Comparing base (c7fc43d) to head (7680133).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #2700      +/-   ##
===========================================
- Coverage    78.13%   78.11%   -0.02%     
===========================================
  Files          311      310       -1     
  Lines        23238    23069     -169     
===========================================
- Hits         18155    18019     -136     
+ Misses        3699     3675      -24     
+ Partials      1384     1375       -9     
Flag Coverage Δ
all-tests 78.11% <90.62%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
cli/start.go 45.61% <ø> (+0.79%) ⬆️
event/event.go 100.00% <100.00%> (ø)
internal/db/collection.go 71.60% <100.00%> (-0.23%) ⬇️
internal/db/collection_delete.go 39.53% <100.00%> (-2.69%) ⬇️
internal/db/config.go 100.00% <ø> (ø)
net/client.go 100.00% <100.00%> (ø)
net/sync_dag.go 82.35% <100.00%> (+0.53%) ⬆️
http/client.go 55.79% <0.00%> (ø)
event/bus.go 97.10% <97.10%> (ø)
internal/db/db.go 60.71% <71.43%> (-2.76%) ⬇️
... and 5 more

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c7fc43d...7680133. Read the comment docs.

Copy link
Contributor

@AndrewSisley AndrewSisley left a comment

Choose a reason for hiding this comment

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

I think we will need to chat about this one - it might be worth separating the changes made outside of the events package from the rest - there are some very good changes in here that should not be delayed/confused with the events package implementation change.

Regarding the events package, it is now simpler, but it is also blocking (in user-space), and less reliable (it discards messages quite rapidly if the subscription buffer fills - the original buffer size was 100 + a 60 second timeout, it is now 10 items and no timeout) and as such I think I will require a bit of convincing in order to see this as a good change.

I do also disagree with the removal of the Channel interface, it was used to allow us to swap out implementations without having to change the calling code. Whilst I think the original simple channel is better than the proposal for our use-cases, it still needs to be improved upon long-term, and different use-cases within the defra code base would likely benefit from different Channel implementations.

tests/integration/state.go Outdated Show resolved Hide resolved
events/bus.go Outdated Show resolved Hide resolved
events/events.go Outdated
Unsubscribe(Subscription[T])
// ConnectEvent is an event that is published when
// a peer connection has changed status.
type ConnectEvent = event.EvtPeerConnectednessChanged
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Since we are in the events package, the Event suffix isn't necessary. We could change events.ConnectEvent, events.PubSubEvent, events.UpdateEvent, events.MergeEvent to events.Connect, events.PubSub, events.Update, events.Merge

Copy link
Contributor

Choose a reason for hiding this comment

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

that would be neat. But for this, following best go practices, would be better to make package name singular event.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@nasdf you changed to event but didn't do my suggestion. Did you just forget or did you decide you didn't like it? :)

Copy link
Member Author

Choose a reason for hiding this comment

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

@fredcarle I'm considering moving the event definitions to their respective packages.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also a good option 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

The downside of defining the events in their respective packages is that if might result in unwanted dependencies or even circular dependencies depending in which package it is defined. MergeEvent being defined in db or net for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I've kept them in the package but renamed as you suggested.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is being discussed in another thread #2700 (comment) :)

events/bus.go Outdated Show resolved Hide resolved
events/bus.go Outdated Show resolved Hide resolved
events/bus.go Outdated Show resolved Hide resolved
events/bus.go Outdated Show resolved Hide resolved
events/bus_test.go Outdated Show resolved Hide resolved
events/events.go Outdated
Comment on lines 19 to 34
const (
// WildCardEventName is the alias used to subscribe to all events.
WildCardEventName = "*"
// MergeCompleteEventName is the name of the database merge complete event.
MergeCompleteEventName = "db:merge-complete"
// UpdateEventName is the name of the database update event.
UpdateEventName = "db:update"
// ResultsEventName is the name of the database results event.
ResultsEventName = "db:results"
// MergeRequestEventName is the name of the net merge request event.
MergeRequestEventName = "net:merge"
// PubSubEventName is the name of the network pubsub event.
PubSubEventName = "net:pubsub"
// ConnectEventName is the name of the network connect event.
ConnectEventName = "net:connect"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: wouldn't it be better to make name an enum (for more explicitness) and make every package (in this case db and net) own the events they generate.
This way this file won't need to change every time a new event in an unrelated package is added.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd like to keep the event names a human readable string for debugging purposes.

Having the events in one package does have the issue you mentioned, but it also makes it much easier to know what events can be subscribed to. There's also the issue of importing an entire package just for an event type.

Copy link
Contributor

Choose a reason for hiding this comment

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

but the package that wants to listed to a specific event usually depends on the package that fires that event as it needs to receive the package-event-specific object.

I'd like to keep the event names a human readable string for debugging purposes

You can declare string enum:

type EventName string

and in db package:

const (
    // MergeCompleteEventName is the name of the database merge complete event.
    MergeCompleteEventName = events.EventName("db:merge-complete")
    // UpdateEventName is the name of the database update event.
    UpdateEventName = events.EventName("db:update")
    // ResultsEventName is the name of the database results event.
    ResultsEventName = events.EventName("db:results")
)

Copy link
Member Author

Choose a reason for hiding this comment

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

but the package that wants to listed to a specific event usually depends on the package that fires that event as it needs to receive the package-event-specific object.

This won't always be the case, and will cause problems since the db package is internal.

You can declare string enum:

I've added an event.Name type.

@nasdf nasdf requested a review from islamaliev June 13, 2024 17:56
}
b.commandChannel <- closeCommand{}
// Wait for the close command to be handled, in order, before returning
<-b.hasClosedChan
Copy link
Contributor

Choose a reason for hiding this comment

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

question: this is thread-safe only under the assumption that all the calls to bufferedBus are done by the same thread. I'm not sure this is the case. Is it?

Otherwise the following scenario is possible:

  1. execution enters Subscribe method, b.isClosed == false so it proceeds but pauses just before b.commandChannel <- subscribeCommand(sub)
  2. concurrently Close is being called, sets b.isClosed to true, pushes b.commandChannel <- closeCommand{}
  3. handleChannel receives closeCommand and executes close(b.commandChannel)
  4. now the thread from step 1 resumes, executes b.commandChannel <- subscribeCommand(sub) and causes a panic.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've replaced the isClosed atomic with a RWMutex. Issue should be fixed now.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's quite a broad mutex, please don't merge yet

Copy link
Member Author

Choose a reason for hiding this comment

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

It should only block when calling close.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I think I understand, and there is no nicer way of handling this that I can spot off the top of my head.

todo: Can you please rename mutex to closeLock or similar, we really really don't want this to be used for anything else.

todo: Can please also document the lock on its declaration in bufferedBus, explaining that it is only locked whilst closing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@AndrewSisley can you explain what you mean by broad? We use the same technique in the memory store and I think it's really fine for this as well.

Copy link
Contributor

@AndrewSisley AndrewSisley Jun 17, 2024

Choose a reason for hiding this comment

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

@AndrewSisley can you explain what you mean by broad? We use the same technique in the memory store and I think it's really fine for this as well.

The lock spans every public call to this struct, it can't really get any broader :) If it only locks whilst closing that's fine, but it would be very damaging if that were to change (hence the documentation and name change requested).

The current name (mutex) is just begging for other places to lock it besides Close in the future.


case unsubscribeCommand:
if _, ok := b.subs[t.id]; !ok {
continue // not subscribed
Copy link
Member

Choose a reason for hiding this comment

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

question: Just pointing incase easy to test, that this path is not tested.


case publishCommand:
for id := range b.events[WildCardName] {
b.subs[id].value <- Message(t)
Copy link
Member

Choose a reason for hiding this comment

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

question: Just pointing incase easy to test, that this path is not tested.

}
for id := range b.events[t.Name] {
if _, ok := b.events[WildCardName][id]; ok {
continue
Copy link
Member

Choose a reason for hiding this comment

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

question: Just pointing incase easy to test, that this path is not tested.

event/bus.go Outdated

// Events returns the names of all subscribed events.
func (s *Subscription) Events() []Name {
return s.events
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: This path is not tested.

@nasdf nasdf requested a review from islamaliev June 17, 2024 18:04
defer bus.Close()

sub, err := bus.Subscribe("test")
assert.Nil(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: it's better to use assert.NoError because it produces better output

Copy link
Member Author

Choose a reason for hiding this comment

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

It was copy pasted, but I've fixed them all.

assert.True(t, true)
}

func TestBufferedBusUnsubscribeTwice(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: some time ago we discussed a naming convention we want to use for our tests and agreed to name them using the following patter:
<test_group>_<condition>_<expectation>.
For this particular test it might be something like TestBufferedBud_IfUnsubscribedTwice_[Succeed|NoError|NoPanic] which makes it much more explicit.
For the test above TestBufferedBus_IfClosed_SubscribersAreNotBlocked

Copy link
Member Author

Choose a reason for hiding this comment

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

I've updated all the test names

Copy link
Contributor

Choose a reason for hiding this comment

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

this is not quite what I meant.
The part is what makes this test different from others. It describes first 2 As (arrange and action).
The part describes the 3rd A: assert.
For example your new test name is TestBus_WildCardDeduplicates_Succeed. In this case part might be IfSubscriptionsOverlap or IfSubscribedToEventAndWildCard or IfSubscribedToMultipleEvents. The part could be ItShouldDeduplicate or ShouldReceiveOnlyOneMessage. So together it would be something like TestBus_IfSubscribedToEventAndWildCard_ItShouldDeduplicate.

I'm not sure if I explained it clearly. So I will give some more examples how the current names can be transformed:
TestBus_EachSubscribersRecievesEachItem_Succeed -> TestBus_WithMultipleSubscriptions_EachShouldReceiveMessage or TestBus_UponPublishWhileMultipleSubscriptions_EachShouldReceiveMessage

TestBus_SubscribersDontRecieveItemsAfterUnsubscribing_Succeed -> TestBus_AfterUnsubscribing_SubscribersDontRecieveItems or TestBus_IfUnsubscribed_ShouldNotReceivePublishedMessages

Sorry for a long lecture. Just wanted to make sure it's clear.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated

assert.NoError(t, err)

bus.Unsubscribe(sub)
bus.Unsubscribe(sub)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: if it is no panicking that you are asserting, why no to use assert.NotPanics?

Copy link
Member Author

Choose a reason for hiding this comment

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

This call itself won't panic because it is simply adding a message to the command channel which is processed asynchronously.

select {
case <-sub.Message():
t.Errorf("should not receive duplicate message")
case <-time.After(1 * time.Second):
Copy link
Contributor

Choose a reason for hiding this comment

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

todo: 1 second is too long. There is no reason for such a long wait.
Why not just use default?:

select {
	case <-sub.Message():
		t.Errorf("should not receive duplicate message")
	default:
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I've decreased the delay. It's required because the publish is asynchronous.

assert.Equal(t, msg2, output2Ch2)
}

func TestBufferedBusSubscribersDontRecieveItemsAfterUnsubscribing(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: tests overall look good. Thanks for taking your time.

@nasdf nasdf requested a review from islamaliev June 17, 2024 21:55
Copy link
Contributor

@islamaliev islamaliev left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks Keenan for the adjustments.

Copy link
Member

@shahzadlone shahzadlone left a comment

Choose a reason for hiding this comment

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

Thanks for testing the requested paths.

LGTM

@nasdf nasdf merged commit 3856b80 into sourcenetwork:develop Jun 20, 2024
38 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/network Related to internal/external network components refactor This issue specific to or requires *notable* refactoring of existing codebases and components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use events system to test networking logic Unsubscribing from db events causes race condition
5 participants