Skip to content

[LIVY-41] Let users access sessions by session name#48

Closed
meisam wants to merge 25 commits into
apache:masterfrom
meisam:LIVY-41-rebased
Closed

[LIVY-41] Let users access sessions by session name#48
meisam wants to merge 25 commits into
apache:masterfrom
meisam:LIVY-41-rebased

Conversation

@meisam

@meisam meisam commented Sep 19, 2017

Copy link
Copy Markdown
Contributor

This commit enables Livy users to access sessions either by names or by auto-generated sessiond id's.

It also prevents users from creating sessions that have the same name.

This commit keeps API change minimal. These are places that API change
is needed:

  • Session and its sub-classes adds a new field, name.
  • RecoveryMetadata and its sub-classes adds a new field, name.
  • SessionManager adds a new method getSession(name: String) which looks sessions up by name.

Task-url: https://issues.apache.org/jira/browse/LIVY-41

This commit  enables Livy users to access sessions either by names or by auto-generated sessiond id's.

It also prevents users from creating sessions that have the same name.

This commit keeps API change  minimal. Thse are palces that API change
is needed:
- `Session` and its subclasses adds a new field, `name`.
- `RecoveryMetadata` and its subclasses adds a new field, `name`.
- `SessionManager` adds a new method `getSession(name: String)` which lookups sessions by name.

A more clean implementation would change the signature of `SessionManager.register` so it returns a proper container around the session value to determine if it failed to register the given session.  For example,
```Scala
def register(session S): Either[S, Throwable]
 ```

Task-url: https://issues.apache.org/jira/browse/LIVY-41
@codecov-io

codecov-io commented Sep 19, 2017

Copy link
Copy Markdown

Codecov Report

Merging #48 into master will increase coverage by 0.36%.
The diff coverage is 80%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master      #48      +/-   ##
============================================
+ Coverage     68.34%   68.71%   +0.36%     
- Complexity      895      902       +7     
============================================
  Files           100      100              
  Lines          5604     5644      +40     
  Branches        840      847       +7     
============================================
+ Hits           3830     3878      +48     
+ Misses         1225     1211      -14     
- Partials        549      555       +6
Impacted Files Coverage Δ Complexity Δ
.../scala/org/apache/livy/server/SessionServlet.scala 68.96% <100%> (+1.89%) 17 <4> (+1) ⬆️
...apache/livy/server/batch/BatchSessionServlet.scala 90.32% <100%> (+1.86%) 3 <0> (ø) ⬇️
...la/org/apache/livy/server/batch/BatchSession.scala 86.17% <100%> (+0.77%) 14 <5> (+1) ⬆️
...va/org/apache/livy/client/common/HttpMessages.java 95.23% <100%> (+0.11%) 0 <0> (ø) ⬇️
...server/interactive/InteractiveSessionServlet.scala 66.93% <100%> (+0.54%) 6 <0> (ø) ⬇️
.../main/scala/org/apache/livy/sessions/Session.scala 72.56% <50%> (-1.02%) 20 <5> (+4)
...e/livy/server/interactive/InteractiveSession.scala 69.04% <71.05%> (+0.68%) 44 <4> (+1) ⬆️
...cala/org/apache/livy/sessions/SessionManager.scala 82.02% <75%> (-0.7%) 24 <1> (+1)
...ain/scala/org/apache/livy/utils/SparkYarnApp.scala 72.53% <0%> (-0.71%) 33% <0%> (ø)
...ain/java/org/apache/livy/rsc/driver/RSCDriver.java 78.81% <0%> (-0.43%) 41% <0%> (-1%)
... and 5 more

Continue to review full report at Codecov.

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

@ajbozarth ajbozarth left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I made a handful of comments, still not 100% behind this, will need some time to think about it.

A couple of general comments:

You'll need to update the Docs to match the API updates.

I think you should use a uniform naming convention for the test classes, it seems each class has it's own naming convention.

checkFn: Option[(String, HttpServletRequest) => Boolean]): Any = {
val sessionId = params("id").toInt
sessionManager.get(sessionId) match {
val idParam: String = params("id")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think re-using the id param for name is a good idea. It should have it's own optional param instead. I believe this is already how you are doing session creation anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't have any strong opinion for or against reusing id param. Here is why:

If we do not reuse id param, we have to create a new REST endpoint for each existing one. Reusing id param allows Livy users access a session seamlessly by its ID or its name through the same endpoint:

sessions/{id} or sessions/{name}
sessions/{id}/statements or sessions/{name}/statements
sessions/{id}/state or sessions/{name}/state
sessions/{id}/log or sessions/{name}/log

If we do not reuse id param, we have to add separate endpoints for accessing sessions by name. Something like
named-sessions/{name}
named-sessions/{name}/statements
named-sessions/{name}/state
named-sessions/{name}/log
or
sessions/by-name/{name}
sessions/by-name/{name}/statements
sessions/by-name/{name}/state
sessions/by-name/{name}/log

or
sessions?name={name}
sessions/statements?name={name}
sessions/state?name={name}
sessions/log?name={name}

@ajbozarth, what are your thoughts on adding a new endpoint?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I checked the code to followup my argument and realized that the way these end-points are written the way you did it is actually the best way. The param name id is not user-facing so the clarity I want can be made in the Docs rather than the code. Given this change to the API though I would like you to double check that the Web UI will support these changes, both that it still works with the id, but also works by using a session name in the url. Actually adding the Session name to the UI can be done in a followup task (which I'm willing to do).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's rename this variable as idOrNameParam to be clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 I'll rename it.

val sessionName: String = createRequest.name match {
case Some(name) if sessionManager.get(name).isEmpty =>
name
case Some(name) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should be an else statement in the first case instead. This usage of match is a bit hacky.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 I'll update this even though I am an advocate of if-guards in case statements.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was thinking an if/else inside the Some(name) case.

s"Choose a different name or delete the existing session."
throw new IllegalArgumentException(msg)
case None =>
s"INTERACTIVE-SESSION-$sessionId"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This wasn't changed in the copy-paste. Also I'm not a fan of this default name, it's a bit verbose. Just batch-$sessionId would suffice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 I'll update it.

s"Choose a different name or delete the existing session."
throw new IllegalArgumentException(msg)
case None =>
s"INTERACTIVE-SESSION-$sessionId"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As above, just session-$sessionId would suffice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

val sessionName: String = createRequest.name match {
case Some(name) if sessionManager.get(name).isEmpty =>
name
case Some(name) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same as above

// Livy checks to make sure another session with the same name does not exist already.
// This case should not happen rarely.
// already exists. But looking up a session name and adding it to the set of existing sessions
// should happen atomically.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Theres some confusing wording this comment block, might be artifacts from changes during development, but it makes the comment difficult to understand.

if (sessionsByName.contains(session.name)) {
val msg = s"Session ${session.name} already exists!"
error(msg)
delete(session)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Discussion Point: Do we want to fail a request with a "bad" name or should we instead give a warning message and create a session with a default name instead? Just an idea, I'm still not 100% sure of this PR's use case.

@meisam meisam Sep 20, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an example use case we have here. I'll ask Prabhu to provide more.

Team AAA has an ETL pipeline everyday at 1pm. The pipeline is a chain of jobs that work on the same dataset. They want to create a session on Livy and use it to run the pipeline. They also want to push the metrics and stats of their pipeline to alerting and monitoring services.
This PR allows them to access the session, manage it, and monitor it with a human readable name like team_aaa_daily_etl.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Your example use case does help me with understanding this PR's purpose, which I am liking more, but not this particular code decision. Is there a good reason we should be failing session creation on a "bad" session name rather than displaying a warning that the name was bad then creating a session with the default naming? I'm not saying either way is better, I am just wondering you reason for doing it this way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think fail the request and return user the specific bad http code would be better then choosing another name for user. Because user want to create a session with name "A" for example, if Livy cannot satisfy user's requirement, then user should choose another name. It is better to let user to decide than letting Livy to handle it.

@jerryshao

Copy link
Copy Markdown
Contributor

@meisam , do you need to change Java and python job API to support this feature? As I remembered it can specify session id to reuse the existing session, so for now we should also support session name.

@meisam

meisam commented Sep 20, 2017

Copy link
Copy Markdown
Contributor Author

@jerryshao Yes, changing the Java and python API is a good idea.

@ajbozarth

Copy link
Copy Markdown
Member

IMPO, for quicker pr reviews, I think adding Java and python API support can be done in a followup PR

}

def startBatch(
name: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would suggest to use Option[String] instead to avoid explicitly set to "null" as below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll update it to avoid explicit null values.

checkFn: Option[(String, HttpServletRequest) => Boolean]): Any = {
val sessionId = params("id").toInt
sessionManager.get(sessionId) match {
val idParam: String = params("id")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's rename this variable as idOrNameParam to be clear.

// it prevents Livy from submitting applications to Spark.
val msg = s"Session $name already exists! " +
s"Choose a different name or delete the existing session."
throw new IllegalArgumentException(msg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the exception here OK, shall we return some HTTP code to rest client about session name duplication?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nice catch, I not sure this would be surfaced to the submitter or not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One case that we should keep in mind about returning an error code.
Assume the existing session s with name daily-job.
If the owner s is requesting a new session with name daily-job, we will reach this point and the user gets a http 400 bad request.
But if a different user is requesting a session s2 with name daily-job, the user gets a 403 Forbidden before reaching this point, because he/she does not have access to /sessions/daily-job/.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, that's true.

val proxyUser = checkImpersonation(createRequest.proxyUser, req)
val sessionId: Int = sessionManager.nextId()
val sessionName: String = createRequest.name match {
case Some(name) if sessionManager.get(name).isEmpty =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We'd better put this code into common place for both interactive and batch session.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is refactored into one place now.


override def recoveryMetadata: RecoveryMetadata =
BatchRecoveryMetadata(id, appId, appTag, owner, proxyUser)
BatchRecoveryMetadata(id, name, appId, appTag, owner, proxyUser)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@meisam what if old recovery metadata which doesn't have name field, I think we should consider the compatibility of old metadata file with new Livy server, would you please check it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What do you suggest? Should we assign such sessions auto-generated names on recovery? Or should we leave them without a name?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm saying with your changes, if new Livy server tries to recover from old data, what will be happened? Will it be failed?

Since the old recovery data doesn't have a session name field, so we could offer an auto-generated name, but we should not fail such scenario.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll add test cases to confirm that Livy can recover from old data.


protected[this] final val idCounter = new AtomicInteger(0)
protected[this] final val sessions = mutable.LinkedHashMap[Int, S]()
private[this] final val sessionsByName = mutable.Map[String, S]()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can use mutable.HashMap to be clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed 👍

if (sessionsByName.contains(session.name)) {
val msg = s"Session ${session.name} already exists!"
error(msg)
delete(session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think fail the request and return user the specific bad http code would be better then choosing another name for user. Because user want to create a session with name "A" for example, if Livy cannot satisfy user's requirement, then user should choose another name. It is better to let user to decide than letting Livy to handle it.

@meisam

meisam commented Sep 29, 2017

Copy link
Copy Markdown
Contributor Author

I think fail the request and return user the specific bad http code would be better then choosing another name for user. Because user want to create a session with name "A" for example, if Livy cannot satisfy user's requirement, then user should choose another name. It is better to let user to decide than letting Livy to handle it.

I like failing the request option more. The only caveat is that with current approach, sometimes we submit the spark App and then kill it later (if two requests arrive at almost the same time, we launch both, but later when we want to add them to sessionManager, we have to keep only one and kill and and discard the other one).

One way to avoid that is to refactor Session, InteractiveSesion, and BatchSession so it does not start the Spark process immediately, so we can start the Spark process after registering them in SessionManager.

Are you okay with this approach?

@ajbozarth

Copy link
Copy Markdown
Member

Are you okay with this approach?

I would have to see the change first but I think this could potentially be ok

@jerryshao

jerryshao commented Sep 30, 2017

Copy link
Copy Markdown
Contributor

I like failing the request option more. The only caveat is that with current approach, sometimes we submit the spark App and then kill it later (if two requests arrive at almost the same time, we launch both, but later when we want to add them to sessionManager, we have to keep only one and kill and and discard the other one).

That's the problem of concurrency, I think we can first check the validity of session name, if it is conflicted, then directly return bad code. We should not postpone such check until session is created.

@jerryshao

Copy link
Copy Markdown
Contributor

One way to avoid that is to refactor Session, InteractiveSesion, and BatchSession so it does not start the Spark process immediately, so we can start the Spark process after registering them in SessionManager.

@meisam I would suggest to leave this work to another PR for the simplicity of review.

meisam added 3 commits October 5, 2017 16:15
Signed-off-by: Fathi Salmi, Meisam(mfathisalmi) <mfathisalmi@paypal.com>
Changing
```
case ... if .... =>
```
to
```
case ... =>
  if ...
```
@jerryshao

Copy link
Copy Markdown
Contributor

@meisam can you please address the comments, thanks!

If no session name is provided, do not use session names.
This makes it possible to recover sessions that were stored with no
name.
@meisam

meisam commented Oct 17, 2017

Copy link
Copy Markdown
Contributor Author

This is the output from last build:

No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself.
Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received
The build has been terminated.

Closing and re-opening this pull request to trigger a new build.

@meisam meisam closed this Oct 17, 2017
@meisam meisam reopened this Oct 17, 2017
@meisam

meisam commented Oct 17, 2017

Copy link
Copy Markdown
Contributor Author

@meisam can you please address the comments, thanks!

@jerryshao I addressed comments in the previous commit.

(scriptPath: String, args: List[String], sparkConf: Map[String, String] = Map.empty)
(f: (LivyRestClient#BatchSession) => R): R = {
val s = livyClient.startBatch(scriptPath, None, args, sparkConf)
val s = livyClient.startBatch(null, scriptPath, None, args, sparkConf)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should here change to None?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's right. I prefer passing None to passing null. I'll fix it.

(testClass: Class[_], args: List[String], sparkConf: Map[String, String] = Map.empty)
(f: (LivyRestClient#BatchSession) => R): R = {
val s = livyClient.startBatch(testLibPath, Some(testClass.getName()), args, sparkConf)
val s = livyClient.startBatch(null, testLibPath, Some(testClass.getName()), args, sparkConf)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll fix this one too.

heartbeatTimeoutInSecond: Int = 0)
(f: (LivyRestClient#InteractiveSession) => R): R = {
withSession(livyClient.startSession(kind, sparkConf, heartbeatTimeoutInSecond)) { s =>
withSession(livyClient.startSession(null, kind, sparkConf, heartbeatTimeoutInSecond)) { s =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed 👍

sessionStore.save(RECOVERY_SESSION_TYPE, recoveryMetadata)
heartbeat()

private val app = mockApp.orElse {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the purpose of below change?

@meisam meisam Oct 19, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Before this change, Livy immediately submits the Spark app upon receiving the request. After this change, Livy has to call Session.start() to submit the Spark app, which means Livy can make sure another session with the same name does not exist before submitting the Spark app.

Without this change, making session names unique is complicated, because if two session names collide, we have only two choices:

  • Kill one of the sessions.
  • Come up with a logic that gives one of the sessions an autogenerated name and guarantees the autogenerated name does not collide with any of the existing/future session names.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation, let me review again.

@meisam

meisam commented Dec 13, 2018

Copy link
Copy Markdown
Contributor Author

Is there a plan to merge this PR? If so, I can fix the recent merge conflicts. Otherwise we should close it for good.

@vanzin vanzin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There seems to be previous feedback that hasn't been addressed.

I took a quick look but it seems to me this is putting both IDs and names into the same namespace. Except I didn't notice any enforcement that IDs and names do not conflict. So if I create a session with name "1", and a later request makes Livy try to create a session with ID 1, what happens?

I think it would be better to keep both concepts separate (e.g. how on /dev/disk you have multiple non-conflicting ways of addressing the same disk).

val driverProcess = client.flatMap { c => Option(c.getDriverProcess) }
.map(new LineBufferedProcess(_, livyConf.getInt(LivyConf.SPARK_LOGS_SIZE)))
driverProcess.map { _ => SparkApp.create(appTag, appId, driverProcess, livyConf, Some(this))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

either in previous line or, if it doesn't fit, use multi-line syntax for the whole closure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In these lines, I only changed the indentation. I think formatting issues should be handled in a separate PR if needed.

} else {
val uriFuture = Future { client.get.getServerUri.get() }

uriFuture onSuccess { case url =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

uriFuture.onSuccess, also in other places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed 👍

def register(session: S): S = {
info(s"Registering new session ${session.id}")
synchronized {
session.name.map { sessionName =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

s/map/foreach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed 👍

val sessionId = params("id").toInt
sessionManager.get(sessionId) match {
val idOrNameParam: String = params("id")
val session = if (idOrNameParam.forall(_.isDigit)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're assuming that a string with all digits is an ID and not a name. I don't see that enforced when creating a session?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I changed this to force session names to have at least one non-digit character.

Session names cannot be numbers.
# Conflicts:
#	server/src/main/scala/org/apache/livy/server/SessionServlet.scala
#	server/src/main/scala/org/apache/livy/server/batch/BatchSessionServlet.scala
#	server/src/main/scala/org/apache/livy/server/interactive/InteractiveSession.scala
#	server/src/test/scala/org/apache/livy/server/batch/BatchServletSpec.scala
#	server/src/test/scala/org/apache/livy/server/batch/BatchSessionSpec.scala
#	server/src/test/scala/org/apache/livy/server/interactive/InteractiveSessionServletSpec.scala
#	server/src/test/scala/org/apache/livy/server/interactive/InteractiveSessionSpec.scala
A session name should have at least one non digit character in it.

@vanzin vanzin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mostly minor things.

str.nonEmpty && str.forall(_.isDigit)
}

name.foreach { idStr =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

idStr is a misleading name since this is not an id.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

renamed it to sessionName

protected implicit val executionContext = ExecutionContext.global

private def isSessionId(str: String): Boolean = {
str.nonEmpty && str.forall(_.isDigit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An empty name should be invalid too, right?

I think it would be clearer to have this code inlined below, as I suggested before. Then that whole block becomes the clear place where session name validation occurs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

An empty name should be invalid too, right?
Right.

While testing what is a good way to separate session names from session IDs, I saw two interesting behaviors in Livy (and Scala/Java).

  • Char.isDigit returns true for digits in all alphabets systems. For example:
scala> "᧑᧒᧓᧔᧕".forall(_.isDigit)
res18: Boolean = true

scala> "᧑᧒᧓᧔᧕".toInt
res19: Int = 12345

scala> "᧑᧒᧓᧔᧕".map(c => Character.getName(c.toInt))
res20: scala.collection.immutable.IndexedSeq[String] = Vector(NEW TAI LUE DIGIT ONE, NEW TAI LUE DIGIT TWO, NEW TAI LUE DIGIT THREE, NEW TAI LUE DIGIT FOUR, NEW TAI LUE DIGIT FIVE)
  • If I create a session with sessionID=1 all these are valid endpoints to the session:
${LIVY_SERVER}:${LIVY_PORT}/sessions/1
${LIVY_SERVER}:${LIVY_PORT}/sessions/01
${LIVY_SERVER}:${LIVY_PORT}/sessions/001
${LIVY_SERVER}:${LIVY_PORT}/sessions/00001
${LIVY_SERVER}:${LIVY_PORT}/sessions/۱
${LIVY_SERVER}:${LIVY_PORT}/sessions/0۱
${LIVY_SERVER}:${LIVY_PORT}/sessions/۰۰۰۱
${LIVY_SERVER}:${LIVY_PORT}/sessions/0۰۰00۰᧑

Should we keep this behavior as is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For now, disallowing empty strings as session names.

view.log shouldEqual log
}

it("should show session properties for sessions without a name") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks very much like the test above. You could have:

Seq(
  (Some(id), None),
  (None, Some(name)
).foreach { case (id, name) =>
  it("should show session properties (id = $id, name = $name)") {
    methodWithTestImpl(id, name)
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

refactored into one helper method.

val batch = BatchSession.recover(m, conf, sessionStore, Some(mockApp))

batch.state shouldBe (SessionState.Recovering)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Check that the recovered session is named?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the check.

view.log shouldEqual log.asJava
}

it("should show session properties for sessions without a name") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same thing here. Better to avoid copy & pasting the existing test and changing a couple of lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

refactored into one helper method.

MockitoMatchers.eq(InteractiveSession.RECOVERY_SESSION_TYPE), anyObject())
}

it("should recover sessions with no name") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Another one that looks like copy & paste of the existing test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

refactored into one helper method.

| "version": 1
|}
""".stripMargin
val batchRecoveryMetadata = stateStore.deserialize[BatchRecoveryMetadata](json.getBytes())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: getBytes(UTF_8)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed.

an[IllegalArgumentException] should be thrownBy manager.register(session2)
manager.get(session1.id).isDefined should be(true)
manager.get(session2.id).isDefined should be(false)
eventually(timeout(5 seconds), interval(100 millis)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test is not needed here, right? It has nothing to do with the session name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed the garbage collection section.

val livyConf = new LivyConf()
val name = "Mock-session"
livyConf.set(LivyConf.SESSION_TIMEOUT, "100ms")
val manager = new SessionManager[MockSession, RecoveryMetadata](

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is now in 3 different places in this file. Should probably be a helper method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored!

@alex-the-man

Copy link
Copy Markdown
Contributor

Have we considered the potential impact of this change for Livy multi-master HA?
To support multi-master HA without this feature, the only thing we need is a distributed atomic counter (which ZK offers).
With this change, we have to enforce session names are unique across multiple masters. This makes multi-master HA more complicated.

@meisam

meisam commented Jan 30, 2019

Copy link
Copy Markdown
Contributor Author

To support multi-master HA without this feature, the only thing we need is a distributed atomic counter (which ZK offers).
With this change, we have to enforce session names are unique across multiple masters. This makes multi-master HA more complicated.

Very good point.
One way to enforce unique session names is to write the name of the session to Zookeeper. If a second session manager tries creating a ZK node with the same node, it will fail.

@meisam meisam closed this Jan 30, 2019
@meisam meisam reopened this Jan 30, 2019
@meisam meisam closed this Jan 30, 2019
@meisam meisam reopened this Jan 30, 2019
@meisam meisam closed this Jan 31, 2019
@meisam meisam reopened this Jan 31, 2019
@vanzin

vanzin commented Feb 5, 2019

Copy link
Copy Markdown

Just to refresh my memory: is multi-master HA a thing? I looked at how session IDs are created today and it doesn't really seem that atomic when you have multiple servers looking at the store. In fact it doesn't seem that atomic even with a single server:

    val id = idCounter.getAndIncrement()
    sessionStore.saveNextSessionId(sessionType, idCounter.get())

That can write the wrong id to the session store if two threads race in just the wrong way.

Anyway, if multi-master is not a thing today, then this looks ok to me.

@meisam

meisam commented Feb 5, 2019

Copy link
Copy Markdown
Contributor Author

I looked at how session IDs are created today and it doesn't really seem that atomic ...

There is a separate PR to make sessionIDs atomic across multiple servers.
#103

@vanzin

vanzin commented Feb 5, 2019

Copy link
Copy Markdown

So I guess pushing this change will require changes in that PR, or vice-versa. Seems this one is farther along.

@meisam

meisam commented Feb 5, 2019

Copy link
Copy Markdown
Contributor Author

So I guess pushing this change will require changes in that PR, or vice-versa. Seems this one is farther along.

No. #103 is not needed for this PR. #103 is part of a bigger PR to implement multi-server HA.

@vanzin

vanzin commented Feb 5, 2019

Copy link
Copy Markdown

I didn't mean it's needed. I'm saying that the "how to have unique names with multiple servers" issue becomes a problem when either PR goes in.

So when I push this, that problem will have to be solved in the other PR. Or vice-versa, if the other PR went in first.

@vanzin

vanzin commented Feb 5, 2019

Copy link
Copy Markdown

Merging to master.

@asfgit asfgit closed this in def6318 Feb 5, 2019
@meisam meisam deleted the LIVY-41-rebased branch February 5, 2019 22:30
kai-chi pushed a commit to logicalclocks/incubator-livy that referenced this pull request Jul 29, 2019
This commit  enables Livy users to access sessions either by names or by auto-generated sessiond id's.

It also prevents users from creating sessions that have the same name.

This commit keeps API change  minimal. These are places that API change
is needed:
- `Session` and its sub-classes adds a new field, `name`.
- `RecoveryMetadata` and its sub-classes adds a new field, `name`.
- `SessionManager` adds a new method `getSession(name: String)` which looks sessions up by name.

Task-url: https://issues.apache.org/jira/browse/LIVY-41

Author: Fathi Salmi, Meisam(mfathisalmi) <mfathisalmi@paypal.com>
Author: Meisam Fathi <meisam.fathi@gmail.com>
Author: Fathi Salmi, Meisam <meisam.fathi@gmail.com>
Author: Fathi, Meisam <meisam.fathi@gmail.com>

Closes apache#48 from meisam/LIVY-41-rebased.
kai-chi pushed a commit to kai-chi/incubator-livy that referenced this pull request Jul 29, 2019
This commit  enables Livy users to access sessions either by names or by auto-generated sessiond id's.

It also prevents users from creating sessions that have the same name.

This commit keeps API change  minimal. These are places that API change
is needed:
- `Session` and its sub-classes adds a new field, `name`.
- `RecoveryMetadata` and its sub-classes adds a new field, `name`.
- `SessionManager` adds a new method `getSession(name: String)` which looks sessions up by name.

Task-url: https://issues.apache.org/jira/browse/LIVY-41

Author: Fathi Salmi, Meisam(mfathisalmi) <mfathisalmi@paypal.com>
Author: Meisam Fathi <meisam.fathi@gmail.com>
Author: Fathi Salmi, Meisam <meisam.fathi@gmail.com>
Author: Fathi, Meisam <meisam.fathi@gmail.com>

Closes apache#48 from meisam/LIVY-41-rebased.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants