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

ensure storage port never conflicts with client port #765

Merged
merged 4 commits into from Apr 7, 2020

Conversation

octonato
Copy link
Member

@octonato octonato commented Apr 6, 2020

This will fix a flaky test in Lagom lagom/lagom#2782.

As seen in Lagom build failure...

Starting Cassandra on port client port: 33477 storage port 33477 host 127.0.0.1 ...

There is a small chance that the storage port conflicts with the client port as both are picked randomly, but not atomically.

The quick fix is to make sure that the storage port never equals the client port and re-try if necessary.

// bad luck? try again
if (port == usedPort) freePort(usedPort)
else port
}
Copy link
Member Author

@octonato octonato Apr 6, 2020

Choose a reason for hiding this comment

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

Unfortunately, freePort is public API, so instead, I'm adding an overloaded method, but private.

Should we deprecate freePort? I don't think we should have this as public API.

Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

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

Worse case freePort is not random but the OS insist on using the same over and over here, resulting in infinite loop.

I think it's better we we pick two ports at the same time, before closing them.

There is a SocketUtil in Akka for this, but that is in testkit and we don't want that dependency from the launcher.

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 will reserve both together while keeping the sockets open and bound during the selection.

@octonato
Copy link
Member Author

octonato commented Apr 6, 2020

This will also fix: lagom/lagom#2688

Same problem, but affecting another Cassandra test.

Copy link
Member

@chbatey chbatey left a comment

Choose a reason for hiding this comment

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

LGTM

// bad luck? try again
if (port == usedPort) freePort(usedPort)
else port
}
Copy link
Member

Choose a reason for hiding this comment

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

👍

@@ -82,6 +82,14 @@ object CassandraLauncher {
port
}

@tailrec
private def freePort(usedPort: Int): Int = {
val port = freePort()
Copy link
Member

Choose a reason for hiding this comment

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

possibly limit the tries? Tho i can't see why it would keep picking the same port

Copy link
Member Author

@octonato octonato left a comment

Choose a reason for hiding this comment

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

I took a different approach. Maybe I'm overthinking it, but here is how it's implemented now.

  • user calls start with 0 and then randomPort, a random port will be selected and used everywhere. randomPort will tell the truth. That's compatible with previous behavior actually.
  • user calls start with non-zeo and then randomPort, the user-specified port will be used everywhere. randomPort will return user-specified port, so not random. This differ from previous behavior.
  • user calls randomPort and then start with 0, a random port will be selected and used everywhere. randomPort will tell the truth. That's compatible with previous behavior actually.
  • user calls randomPort and then start with non-zero, the user-specified port will be used everywhere. randomPort will tell a lie becasue it will be fixed to a random value that is discarded. That's compatible with previous behavior actually.

val realHost = host.getOrElse("127.0.0.1")
val realPort = if (port == 0) randomPort else port
val storagePort = freePort()
val realHost = host.getOrElse(DEFAULT_HOST)
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 think there was a flaw here. If the user passes a host, we should use it when binding the ports. See below.

val storageSocket = ServerSocketChannel.open().socket()

try {
clientSocket.bind(new InetSocketAddress(host, requestedPort))
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 explicitly bind the requested port, even if it's not 0. We must ensure that this port is free and that storage port won't conflict with it.

case (Failure(ex1), Failure(ex2)) => throw new RuntimeException(s"Failed to close sockets: client '${ex1.getMessage}', storage '${ex2.getMessage}'" )
case (Failure(ex1), _) => throw new RuntimeException(s"Failed to close client-port socket: '${ex1.getMessage}'" )
case (_, Failure(ex2)) => throw new RuntimeException(s"Failed to close storage-port socket: '${ex2.getMessage}'" )
case (_, _) => // we are fine, all closed
Copy link
Member Author

Choose a reason for hiding this comment

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

Not so sure if really important to do this.

Maybe

t1.flatMap(_ => t2).get 

would be enough (?).

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

LGTM, I guess you want to backport to release-0.x branch so it's included in next 0.10x

@octonato
Copy link
Member Author

octonato commented Apr 6, 2020

I guess you want to backport to release-0.x branch so it's included in next 0.10x

Yes, we want to backport it.

@octonato
Copy link
Member Author

octonato commented Apr 6, 2020

@chbatey, I have changed this PR quite substantially, you may want to give a second review.

@octonato
Copy link
Member Author

octonato commented Apr 7, 2020

Something pretty wrong with my impl:

[info] The CassandraLauncher
Starting Cassandra on port client port: 0 storage port 0 host 127.0.0.1 java version 1.8.0_242-b08
4/06 15:03:42 ERROR[main] o.a.c.s.StartupChecks
      [] - cassandra.jmx.local.port missing from cassandra-env.sh, unable to start local JMX service.
[info] - must support forking *** FAILED *** (45 seconds, 128 milliseconds)
[info]   java.lang.RuntimeException: Cassandra did not start within 45 seconds
[info]   at akka.persistence.cassandra.testkit.CassandraLauncher$.tryConnect$1(CassandraLauncher.scala:409)
[info]   at akka.persistence.cassandra.testkit.CassandraLauncher$.waitForCassandraToListen(CassandraLauncher.scala:413)
[info]   at akka.persistence.cassandra.testkit.CassandraLauncher$.startForked(CassandraLauncher.scala:337)
[info]   at akka.persistence.cassandra.testkit.CassandraLauncher$.start(CassandraLauncher.scala:286)
[info]   at akka.persistence.cassandra.testkit.CassandraLauncher$.start(CassandraLauncher.scala:213)
[info]   at akka.persistence.cassandra.testkit.CassandraLauncherSpec.$anonfun$new$2(CassandraLauncherSpec.scala:52)

Looking...

@octonato
Copy link
Member Author

octonato commented Apr 7, 2020

TIL: it turns out that compareAndSet checks for the same instance. 🤦‍♂

A few comments though:

  • I considered using an Option to make it clear what should be the value when ports were not yet set, ie: None. But it adds too much noise without much value.
  • I also considered using null. It's ugly, but we never use that value when it's null. I resisted to this one by pure prejudice.
  • in the end, I chose for a fixed value initialPortsValue

I'm happy to change it if others have different opinions.

@@ -68,20 +70,69 @@ object CassandraLauncher {

private var cassandraDaemon: Option[Closeable] = None

private val DEFAULT_HOST = "127.0.0.1"

private val initialPortsValue = (0, 0)
Copy link
Member

Choose a reason for hiding this comment

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

right

@patriknw patriknw added this to the 1.0-RC2 milestone Apr 7, 2020
@octonato octonato merged commit 451d657 into akka:master Apr 7, 2020
@octonato octonato deleted the fix-free-port-selection branch April 7, 2020 12:26
octonato added a commit to octonato/akka-persistence-cassandra that referenced this pull request Apr 7, 2020
ensure storage port never conflicts with client port
octonato added a commit that referenced this pull request Apr 7, 2020
Manual backport of #765 on release-0.x
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants