Ability to create uninitialized sessions
Description
Environment
Pull Requests
relates to
Activity

Alex Dutra July 13, 2020 at 11:34 AM
Closing for now as the eager session initialization pattern is good enough for the Quarkus extension. If someone disagrees and thinks this should be implemented, feel free to reopen and explain your reasons.

Olivier Michallat June 30, 2020 at 12:04 AM(edited)
users will have to choose between the LazySession approach and buildAsync() for now
Just to clarify: they don't have to choose, lazy init and async init are two completely orthogonal concerns.
// lazy + sync
Supplier<CqlSession> sessionSupplier = memoize(sessionBuilder.build());
// eager + async
CompletionStage<CqlSession> sessionFuture = sessionBuilder.buildAsync();
// lazy + async
Supplier<CompletionStage<CqlSession>> sessionFutureSupplier = memoize(sessionBuilder.buildAsync());
(using a hypothetical memoize
method that builds a LazyReference<T>
similar to the LazySession
I posted earlier; note that most injection frameworks will handle this for you)
Joao Reis June 29, 2020 at 7:58 PM
I think we are mixing two discussions here and it looks like we disagree on both of them. One is about whether the use case is valid and whether we want to support it. The other one is about how feasible it is to implement it in a minor release without breaking the API (or even in a major release without changing too much of the API).
I agree that the current API and its implementation makes this "model" super tricky to support because it would probably create confusion somewhere. However, I still think we should do our best to support the use case and at the very least plan our next steps to support it (even if it's only in a major release in 10 years).
If session initialization does not block application startup, then user requests might start processing before the session is ready.
The driver has control over that, it can block (sync) or chain the future (async) so that the user requests are only processed after the initialization is done.
I think the blocking init is not a big deal, since you'd usually wait until your backend is ready before accepting user requests anyway.
I've seen deployment models that work best with information retrieved from HTTP calls (e.g. health check endpoints). If the application initialization is synchronous and it depends on the blocking session initialization it probably means that it won't be able to serve those HTTP requests during that time.
Also this would cause the whole application to fail because of one dependency when in reality there could be other parts of the application that don't depend on the session and could have continued working just fine.
Just to reiterate, I'm well aware that the current implementation causes several issues that are very real, I'm currently dealing through those and doing some changes to the API on the C# driver which currently has a very similar Metadata API to the java driver AFAIK.
The outcome of this discussion could be that there is no good enough option to implement this in a minor release so users will have to choose between the LazySession approach and buildAsync() for now but that shouldn't stop us from recognizing that the user experience will not be the best and that it should be improved in a future major release.

Olivier Michallat June 29, 2020 at 5:59 PM(edited)
To play nice with this model, the driver should not prevent the application from starting. While it is true that users can already use buildAsync(), users should not have to be "forced" to deal with a session future everywhere in their application.
Sorry if I'm repeating myself, but these are two contradicting goals. If session initialization does not block application startup, then user requests might start processing before the session is ready. The correct and idiomatic way to express that "component X will be available at some point later" is to use a future. Hiding this behind a seemingly synchronous API only creates confusion, and you'll hit a wall sooner or later.
This is not API purism, the issues are real: imagine that you are creating a session asynchronously, and one part of your application wants to know specifically when the initial version of the metadata becomes available. With the current API, this looks like this:
CompletionStage<CqlSession> sessionFuture = CqlSession.builder().buildAsync();
sessionFuture.thenApply(Session::getMetadata).thenAccept(metadata -> ... );
With the model proposed in this ticket, the first line would return a CqlSession
instance directly, and initialize it in the background. But what should Session.getMetadata
do before that background initialization is complete? We have two options:
block. This defeats the whole purpose of async, in a way that is very deceptive for users. Basically the same issues as the driver 3 result set API (https://datastax-oss.atlassian.net/browse/JAVA-2801#icft=JAVA-2801).
return an empty instance, as suggested by Alexandre above. But how do you write the snippet above? The only way to find out when the metadata becomes "not empty" is to poll at regular intervals. Doing that without blocking a thread will be way more complicated than chaining two future callbacks; also it's dumb because you're basically reimplementing a future.
For completeness I should also add that a 3rd option would be to make getMetadata
return a future. But:
it breaks the API
it does not reflect the reality. Having a first version of the metadata is a precondition for considering a session "ready", so there will never be a delay in practice (unless we return a session object that is not ready).
it only moves the future one method call away, so it's not really addressing the problem.
They might not even want to use futures at all.
In that case, they will call the synchronous session.execute
. With the model proposed by Alexandre: "any call to other methods in the Session interface that are inherently synchronous would block until session is initialized".
So you end up blocking in exactly the same place:
// With JAVA-2832:
uninitializedSession.execute("..."); // blocks until the session is initialized
// With current driver + lazy reference
lazySession.get().execute("..."); // blocks until the session is initialized
I think there's a case for 3 different models:
everything synchronous
synchronous initialization, fully asynchronous user requests
everything fully asynchronous
If I wrote an async REST service (e.g. JAX-RS @Suspended
and AsyncResponse
), I would consider 2 first. Indeed it's slightly more convenient to have a session
instance instead of chaining your calls to a future every time, and I think the blocking init is not a big deal, since you'd usually wait until your backend is ready before accepting user requests anyway.
But if you need 3, I'm sorry you'll have to deal with futures. They're just the manifestation of the fact that you're doing async. Hiding the truth behind a questionable API won't change the truth.
Joao Reis June 29, 2020 at 11:12 AM
I've been looking into this for the C# driver so I'll offer my 2 cents.
The Quarkus model seems to be one that is growing in popularity in the .NET world as well:
Application startup as fast as possible
Check if application is ready for requests via HTTP calls (readiness / health checks) and/or perform some kind of warmup also via HTTP calls before routing user traffic to the application
To play nice with this model, the driver should not prevent the application from starting. While it is true that users can already use buildAsync(), users should not have to be "forced" to deal with a session future everywhere in their application. They might not even want to use futures at all.
If users do want to use executeAsync() then dealing with a session future is not as bad but it's still something that adds frustration with no real benefit.
Supporting this without breaking the API is the real challenge and how feasible it is should be the main point for this discussion imo. In any case we should be able to agree that there are real use cases that would benefit quite a bit from this. Also it would make it much easier to integrate the driver with other libraries/frameworks in the future that might follow the same model.
Just a heads up, the LazySession approach would have a lot of problems in .NET because it would cause a lot of threadpool threads to block which could actually prevent the session initialization from finishing successfuly (shared threadpool). I have no idea if this is true in Java or not though. Users would be forced to call .get() on every call which is not as bad as having to deal with a future imo but still not ideal.
Details
Details
Assignee
Reporter

While in driver 3 it is possible to create a disconnected session, in driver 4 this is currently not possible.
The ability to create uninitialized sessions has come up many times, but with the recent popularity of microservices architectures, it seems that the ability to create a
CqlSession
instance without initializing it has become more pressing.An example is the Cassandra Quarkus extension. We had recently to implement eager session initialization at startup, to avoid the common pattern of "initialization on first access". Init on first access seems to be the preferred pattern for Quarkus extensions, but the driver session can only do this by blocking the calling thread making the first access, until the session is fully initialized. Depending on which thread makes that call, this may or may not be acceptable.
Granted, eager init on startup solves this problem, but at the cost of making the entire Quarkus app startup depend on whether or not the driver session was able to initialize itself: any failure to connect to Cassandra triggers the entire microservice startup failure. Again, depending on the app, this may or may not be acceptable.
Instead, what seems to be the right choice is to trigger initialization in the background (this we have already), but on first access, assuming that it corresponds to an asynchronous query, then it should not be required to block the calling thread, it should be enough to chain the session initialization future to the asynchronous query future. Of course if the query is synchronous, then it would be ok to block until end of initialization, as we do now.
It's probably not going to be an easy task to reverse-engineer driver 4 to fit into this initialization model, but it seems that the demand for it will only grow.
More specifically I'm thinking of the following design principles:
A new method
SessionBuilder.buildLazy()
would return a non initialized session.Calling the new method
session.init()
on such a lazy instance would trigger a background initialization.While the session isn't initialized:
Any call to async or reactive query methods would return immediately, as now, but would chain the underlying asynchronous query to the session init future.
Any call to sync query methods would block until the session is initialized.
Any call to methods like
getMetadata
,getMetrics
,getKeyspace
,getContext
andisSchemaMetadataEnabled
would return normally, but could potentially return empty objects (e.g.getMetadata
would returnDefaultMetadata.EMPTY
).Any call to other methods in the
Session
interface returningCompletionStage
would be implemented by chaining the session init future to the returned future (e.g.setSchemaMetadataEnabled
orsetKeyspace
).Finally, any call to other methods in the
Session
interface that are inherently synchronous would block until session is initialized (e.g.refreshSchema
).A secondary goal would be to guarantee that non-API methods also behave correctly when invoked on a non-initialized session (e.g. any of the methods in
DefaultSession
,InternalDriverContext
,DefaultTopologyMonitor
, etc.). Indeed the use case here is merely to be able to call API methods on non-initialized sessions without blocking the thread. Anyone accessing the driver internal API on a non-initialized session would doing so at his own risk, and that is fine.