-
Notifications
You must be signed in to change notification settings - Fork 5
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
Use Go in a more idiomatic way #126
Changes from 14 commits
a439489
8d0a14e
4386f4d
11d83f6
36b3fc7
5376a7a
b03eb1a
9486575
9315323
4ce8b07
974f469
20b5085
94d0b62
4ae95a8
eff418b
b40d6ad
7c4ea8f
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,85 @@ | ||
package channel | ||
|
||
import ( | ||
"errors" | ||
"sync/atomic" | ||
) | ||
|
||
var ErrSinkSealed = errors.New("The channel is sealed") | ||
|
||
// SinkWithSender is a helper struct that allows to send messages to a message sink. | ||
// The SinkWithSender abstracts the message sink which has a certain sender, so that | ||
// the sender does not have to be specified every time a message is sent. | ||
// At the same it guarantees that the caller can't alter the `sender`, which means that | ||
// the sender can't impersonate another sender (and we guarantee this on a compile-time). | ||
type SinkWithSender[SenderType comparable, MessageType any] struct { | ||
// The sender of the messages. This is useful for multiple-producer-single-consumer scenarios. | ||
sender SenderType | ||
// The message sink to which the messages are sent. | ||
messageSink chan<- Message[SenderType, MessageType] | ||
// A channel that is used to indicate that our channel is considered sealed. It's akin | ||
// to a close indication without really closing the channel. We don't want to close | ||
// the channel here since we know that the sink is shared between multiple producers, | ||
// so we only disallow sending to the sink at this point. | ||
sealed chan struct{} | ||
// A "mutex" that is used to protect the act of closing `sealed`. | ||
alreadySealed atomic.Bool | ||
} | ||
|
||
// Creates a new MessageSink. The function is generic allowing us to use it for multiple use cases. | ||
// Note that since the current implementation accepts a channel, it's **not responsible** for closing it. | ||
func NewSink[S comparable, M any](sender S, messageSink chan<- Message[S, M]) *SinkWithSender[S, M] { | ||
return &SinkWithSender[S, M]{ | ||
sender: sender, | ||
messageSink: messageSink, | ||
sealed: make(chan struct{}), | ||
} | ||
} | ||
|
||
// Sends a message to the message sink. Blocks if the sink is full! | ||
func (s *SinkWithSender[S, M]) Send(message M) error { | ||
if s.alreadySealed.Load() { | ||
return ErrSinkSealed | ||
} | ||
|
||
messageWithSender := Message[S, M]{ | ||
Sender: s.sender, | ||
Content: message, | ||
} | ||
|
||
select { | ||
case <-s.sealed: | ||
return ErrSinkSealed | ||
case s.messageSink <- messageWithSender: | ||
return nil | ||
} | ||
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. You do not know which of the two cases is executed at runtime. With 'default' you make sure that the case is checked first. select {
case <-s.sealed:
return ErrSinkSealed
default:
s.messageSink <- messageWithSender:
return nil
} 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. But is it not different semantically? I.e. what if the sender gets to the select {
case <-done:
return OkRecipientDoesNotExpectNewMessages
case s.messageSink <- messageWithSender:
return nil
} Though your concern is valid, I tried to catch this case by checking the atomic variable at the beginning of the function (this does not guarantee that those who called 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. Ok got it better send a message more than blocking. |
||
} | ||
|
||
// Seals the channel, which means that no messages could be sent via this channel. | ||
// Any attempt to send a message after `Seal()` returns will result in an error. | ||
// Note that it does not mean (does not guarantee) that any existing senders that are | ||
// waiting on the send to unblock won't send the message to the recipient (this case | ||
// can happen if buffered channels are used). The existing senders will either unblock | ||
// at this point and get an error that the channel is sealed or will unblock by sending | ||
// the message to the recipient (should the recipient be ready to consume at this point). | ||
func (s *SinkWithSender[S, M]) Seal() { | ||
if !s.alreadySealed.CompareAndSwap(false, true) { | ||
return | ||
} | ||
|
||
select { | ||
case <-s.sealed: | ||
return | ||
default: | ||
close(s.sealed) | ||
} | ||
} | ||
|
||
// Messages that are sent from the peer to the conference in order to communicate with other peers. | ||
// Since each peer is isolated from others, it can't influence the state of other peers directly. | ||
type Message[SenderType comparable, MessageType any] struct { | ||
// The sender of the message. | ||
Sender SenderType | ||
// The content of the message. | ||
Content MessageType | ||
} |
This file was deleted.
This file was deleted.
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.
matrixClient.RunSyncing
running endless until a panic happen. Is that right?We could avoid this if we change the signature like this:
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.
Ah, I see. Do I get it right that the advantage is that we don't panic from inside of a package, but do it in
main
instead?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.
We could decide that by what kind of error occurred. I think we should avoid panic if not needed.
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.
Yeah, makes sense, though we only have one type of error. But please confirm that my understanding is correct: the reason why we're doing this is that it would allow us to avoid panic inside a package? - If so, I agree, but couldn't we solve it easier by just returning this error instead of panicking? I.e. I can slightly change the
RunSyncing()
so that it does not panic once syncing stopped, but instead just returns the error back to the caller.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.
RunSyncing()
acts as the main loop.errc
is a kind of done channel that could interrupt the loop gracefully. Additionallyerrc
contains the reason for the termination. Withnil
it was an intentional abort and witherror
an abort because an issue.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.
Right. But we could simply return an error instead of a panic which would be the same effect, right?
Anyway, I pushed a couple of new changes, so that:
errc
is not required in that particular case, but could be added on top of it if we ever need it?).done
inside a conference state anymore (as you pointed out, we only use it from theprocessMessages()
loop to close it once the loop is over, so I just pass it as a local variable. That way no other function can [by accident] close a channel that it's not supposed to close 👍Ptal 🙂