-
Notifications
You must be signed in to change notification settings - Fork 6
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
Do not background server in ping-pong test #29
Conversation
_ <- IO(assertEquals(res, "pong")) | ||
} yield () | ||
|
||
serverCh.bind(new InetSocketAddress(0)) *> server.both(client).void |
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.
The solution is simple: instead of running the server in the background
to the client (where its exceptions are ignored) instead run both
concurrently with equal importance.
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.
I feel like I am sitting exams in a concurrency course ;-)
Insert usually humble "I could be wrong. If so sorry"
I think you have the right concern but are introducing a race condition here.
The server needs to have completed initialization and be
known to be in accept before the client starts (well before it issues its connect()
but the client prologue code appears short & fast).
To check my grasshopper understanding of "*>", it says (from the cats-effedt IO API)
*>[B](that: IO[B]): IO[B] Runs the current IO, then runs the parameter, keeping its result.
This means that the serverCh.bind
has run to completion and written all the way to memory? Client uses
serverAddr <- serverCh.localAddress. Is there an easy way for the
serverCh.localAddress`
to be passed to the client rather than globally? This would guarantee sequencing and make it evident.
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.
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.
I think you have the right concern but are introducing a race condition here.
Yes, after writing this I became worried there is a race condition here myself. Let me think through it again 😂
This means that the
serverCh.bind
has run to completion and written all the way to memory.
Yes, that's right. bind
must complete before it continues.
Is there an easy way for the
serverCh.localAddress
to be passed to the client rather than globally?
Sure, we could def client(serverAddress)
and write it like this maybe?
serverCh.bind(new InetSocketAddress(0)) *>
serverCh.localAddress.flatMap { serverAddress =>
server.both(client(serverAddress)).void
}
But I'm not convinced this will make much difference: we are still relying on serverCh.bind
to complete, because otherwise won't serverCh.localAddress
just return junk to us?
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.
I think we are making progress but there is a mélange of concerns.
-
You are correct
serverCH.bind
must complete. -
Using the argument, eliminates the requirement that the bind result
must have reached possibly shared, possibly distribute memory. -
Unless there is some semantic that the
server
part of.both()
happens
before the (client) part, I think there is still a race.I believe that there is a complex way to take a lock on a common, shared,
volatile variable before the.both()
. Have the client aquire the lock (meaning
that it does a synchronous wait until it gets it. The client then does a tiny (2 second?)
delay, then begins execution. Meanwhile, the server releases the lock immediately
before itsaccept
. Most example are written for single-stream human speed "start server; start client"
Here we are dealing with probably parallel machine speed.Much as is an eyesore, I think the economic fix is explicit
.delay()
. start_server.delay(10).start_client_in_parallel
I think the cats-effect IO API has a.delay()
method. Yes I know that we are trying to do async code here.
of 10 (5?) seconds is enough
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, thanks for that detailed explanation. I understand your concerns better now, specifically about shared memory across threads. However, because the Scala Native runtime is single-threaded, this should not be a problem at this time (although in the future we will have to think hard how to make these implementations thread-safe).
3. Unless there is some semantic that the
server
part of.both()
happens
before the (client) part, I think there is still a race.
I'm confused why this needs to be the case. The very first step in server
is to accept
the client socket. There is no way for this step to proceed without running the client
concurrently.
val server = serverCh.accept.use { ch => |
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.
I will study this more in the light. In another discussion you posted
the code for "local and remote addresses".
There, the server socket is definitely "listening". The same is true
here. I think there is a time window, where the client can connect
to the ServerSocket (at the indicated port) and the TCP handshake
is 'held" for a short period of time. I need to check, with a clear
head and clear mind if my understanding is right and how long
"short" is. Memory has it that it is not forever.
I think a well-defined, if ugly, and obvious happens-before leads to
lower support costs.
More later.
Thank you for chasing this.
I am happy to hold on this PR until we can resolve #21 as well. I do not want that problem to go underground, as I understand it to be more serious. |
#21 is more serious in the testSuite environment. That one means that users in the wild can get re: this one. I went back and studied how a connection is established. The longer story is that JVM-ish ServerSocket When accept() is eventually executed, the connection will be taken of the listen queue In the absence of any other timeouts, client writes before accept will continue to In the current case, there is only one possible client, writing a small amount All of this depends upon the assured 'happens-before' of bind-with-listen. I believe that "this is (now) happening in ping-pong`" I usually write server code which is flooded with requests when the gates open, |
I think this hinges upon "what does it mean to complete", with the argument, it is pretty clear, at least to me, that the I may be excessive here and do not mean to make your life difficult. I am trying to reduce your support costs, |
These are great points, thank you for the detailed explanations. I completely understand your concerns about memory publication: we agonize over details like this in Cats Effect JVM (a tangent but there is a fantastic story/talk about one such investigation :) The part I still can't wrap my head around is how these issues can come into play in a single-threaded runtime, which is the only option in Scala Native today. Furthermore, if these issues do come into play, then I have no idea how we can solve it, as Scala Native currently offers no way of defining volatile or atomic variables. |
Sure as dancing leads to sin; concurrency leads to parallelism. A lot depends upon how much you believe your abstraction.
At the level of the Thing change and probably cost you a long week of panic coming at the worst of times.
You are right, in the general case, we need In this specific case, and its relatives in TcpSuite.scala, and in many/most other places, I set out this morning with my hot coffee to test this PR in my "known broken by me" |
That's the thing: we do know the underlying implementation is single-threaded:
I definitely agree this will be a problem in the future when multithreading is at play. At that time, Scala Native will also give us the necessary tools to solve this problem, such as volatiles and atomics. |
OK, I got some time-on-task and exercised this PR TL;DR Sorry, it does not solve the problem of the test failing when
|
Would you mind pushing up a branch with this? So I can take a look. |
Are you running an HTTPs server on your machine? If you are using an HTTPs server, or do not know, you should
I can do full branch if you like. Mine has printf's in it so that I can |
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.
LGTM
Our discussions convinced me that the serverCh.localAddress
will have a proper value
by the time it is used.
Fixes #28.
I wonder if #21 is related, but in that case it seems it was hanging if there was an error in the client 🤔