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
Port Controller from Spray to Akka #2218
Conversation
dubee
commented
May 5, 2017
- Refactor controller to use Akka as Spray is no longer supported
Need to consider if we want to replace the existing controller with the port to Akka, or have tons of duplicate code hanging around for version 2 of the controller. |
Still need to port over triggers, activations, rules, and packages. Opening the PR for comments though. |
private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt | ||
|
||
/** Custom deserializer for timeout query parameter. */ | ||
private implicit val stringToTimeoutDeserializer = new FromStringDeserializer[FiniteDuration] { |
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.
Need to fix this to deserialize FiniteDuration with Akka.
I imagined - where necessary - to refactor the handlers from v1 so they are free of routing dependencies and so v1 vs v2 just become routing specific, with serializers for the corresponding http response types. Definitely do not want to duplicate/clone-and-own all of v1 as v2. |
Can we port v1 as is, without going to v2? Benefits include:
|
245b09b
to
2d363da
Compare
Quick q on the procedure here: Do we want to copy all the code or do we want to migrate v1 to akka-http in-place? Asking specifically because I'd like to keep the git history of these files intact. |
+1 I do think there's a way to do the migration to Akka-http without all this copying. |
POST requests currently require a payload. Otherwise, an error response is generated. Not sure if this is new in Akka, @rabbah. Will have to figure it out. Another defect is that generated error responses are not currently JSON formatted. |
For the port, I plan on overwriting the existing controller so that there isn't a lot of duplicate code. Will be able to reuse the existing test suites this way. For now, I'm just working with the duplicate code until I get everything converted over. Just need web actions now. |
Side note: keep getting a consul test failure in Travis when running two controllers. |
65da9c0
to
c5b7ef9
Compare
8620bed
to
3add84e
Compare
Currently I have replaced the Spray controller with the Akka controller. However, controller unit tests are failing as they are still using Spray, so these tests will have to be updated. Also the BasicHttpService will have to be updated to use Akka since the controller and invoker extend it. |
dd145eb
to
4021c82
Compare
I'm going to squash all the commits into one commit due to the nightmare that just resulted after rebasing against upstream. |
f213d4e
to
0ec80f9
Compare
508312c
to
2af8d4b
Compare
Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset) => | ||
val decoded = data.decodeString(charset.nioCharset.name) | ||
|
||
Try { |
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.
perhaps this should check decoded.length > 0
and decode else return JsObject and skip the try/catch in that case.
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.
FWIW, here is the unmarshaller I wrote while debugging performance issues (it's the original one copied and added the data.size == 0
case)
implicit val entityToJsObject: FromEntityUnmarshaller[JsObject] =
Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset) =>
if (data.size == 0) {
JsObject()
} else {
val input =
if (charset == HttpCharsets.`UTF-8`) ParserInput(data.toArray)
else ParserInput(data.decodeString(charset.nioCharset))
JsonParser(input).asJsObject
}
}
DebuggingDirectives.logRequest(logRequestInfo _) { | ||
DebuggingDirectives.logRequestResponse(logResponseInfo _) { | ||
routes | ||
handleRejections(customRejectionHandler) { |
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.
@dubee why did the directive order change?
|
||
/** Assigns transaction id to every request. */ | ||
protected val assignId = extract(_ => transid()) | ||
|
||
/** Rejection handler to terminate connection on a bad request. Delegates to Spray handler. */ | ||
|
||
protected def customRejectionHandler(implicit transid: TransactionId) = RejectionHandler { |
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.
is my understanding correct that the prioritization above lowers the priority on the auth rejection?
|
||
protected def customRejectionHandler(implicit transid: TransactionId) = RejectionHandler { | ||
case rejections => { | ||
logging.info(this, s"[REJECT] $rejections") |
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.
are we losing the rejection log message?
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'm ok dropping this - it's of nominal value.
case Success(e) => Right(e) | ||
case Failure(t) => Left(MalformedContent(t.getMessage)) | ||
case Success(e) => e | ||
case Failure(t) => throw new IllegalArgumentException(s"Parameter is not a valid value for a entity name: $value") |
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'd move all three throw here to messages to Messages.
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.
pushed a commit to do this
case t => Left(CustomRejection(ServiceUnavailable)) | ||
} | ||
def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]) = { | ||
authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds => |
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 code above tries to return a more informative error when authentication fails because the database is not available. I think we are losing this feature now. I don't think there was a test for this (it will require mocking a db connection and failing it).
you can simulate this locally and verify this is a small change in behavior - if i'm right, then please open an issue to address it separately (this time with a test).
edit: confirmed that the new logic reports an internal server error if the database connection is severed instead of service unavailable.
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.
opened #2607
protected[controller] trait RespondWithHeaders extends Directives { | ||
val allowOrigin = `Access-Control-Allow-Origin`(AllOrigins) | ||
val allowOrigin = `Access-Control-Allow-Origin`.* |
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.
🙈
complete(OK, trigger.withoutRules) | ||
} | ||
|
||
implicit val entityToJsObject: FromEntityUnmarshaller[JsObject] = |
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.
this method is repeated twice?
occurs also in Actions.scala.
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.
pushed a commit to address this.
if (contentType.mediaType.binary) { | ||
Try(JsString(Base64.getEncoder.encodeToString(data.toByteArray))) match { | ||
case HttpEntity.Strict(contentType, data) => | ||
// application/json is not a binary type in Akka, but is binary in Spray |
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.
this will make a lot of users happy 🎉
opened #2606.
|
||
} | ||
} | ||
|
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.
what are we losing by removing these tests?
status should be(RequestEntityTooLarge) | ||
val expectedErrorMsg = Messages.entityTooBig(SizeError( | ||
fieldDescriptionForSizeError, | ||
(largeEntity.length + 13).B, | ||
(largeEntity.length + 8).B, |
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.
+5B again - guessing quotes are the difference.
@dubeejw great job on getting all the issues sorted out in this migration! |
Fix logging of request.
} | ||
} | ||
} | ||
case CustomRejection(status, cause) :: _ => complete(status, ErrorResponse(cause, transid)) |
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.
this in particular was used for the auth failure mode #2607.
we can delete CustomRejection since it will be unused now - assuming we keep this behavior.
@@ -214,7 +218,7 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi { | |||
Parameters(key.toString, "a" * 10) | |||
} reduce (_ ++ _) | |||
val content = s"""{"parameters":$parameters}""".parseJson.asJsObject | |||
Put(s"$collectionPath/${aname}", content) ~> sealRoute(routes(creds)) ~> check { | |||
Put(s"$collectionPath/${aname}", content) ~> Route.seal(routes(creds)) ~> check { |
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.
just wondering why would want to use curly braces syntax instead of omitting them?
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.
Around aname? Historically this may have been something else then copy paste. Should be removed.
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.
sorry, yep around aname variable.
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.
👍 if you want to remove them and send a pr
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.
yup, I'll do that ;)
case Failure(usr: UnsuccessfulResponseException) if usr.response.status == StatusCodes.NotFound => | ||
Http().singleRequest(request).map { | ||
case HttpResponse(StatusCodes.OK, headers, entity, _) => | ||
logging.info(this, s"successfully invoked ${rule.action} -> ") |
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.
looks like we missed this - was previously logging the activation id.
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.
also looks like something is not quite right here - even after firing the trigger correctly, inspecting the controller logs shows:
[2017-08-13T13:48:32.233Z] [WARN] [#tid_7] [TriggersApi] action guest/a could not be invoked due to Source(SourceShape(StatefulMapConcat.out), CompositeModule [3c60a25e] Name: iterableSource Modules: (singleSource) GraphStage(SingleSource(List(ByteString(123, 34, 97, 99, 116, 105, 118, 97, 116, 105, 111, 110, 73, 100, 34, 58, 34, 98, 97, 53, 99, 57, 51, 55, 102, 51, 97, 57, 101, 52, 48, 56, 52, 56, 98, 49, 102, 100, 54, 99, 53, 54, 54, 52, 55, 48, 100, 50, 50, 34, 125)))) [41c5bf50] (unnamed) [703a0c97] copy of GraphStage(StatefulMapConcat) [50915aa5] Downstreams: single.out -> StatefulMapConcat.in Upstreams: StatefulMapConcat.in -> single.out MatValue: Atomic(singleSource[41c5bf50]))
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller
* Port Controller from Spray to Akka * Increase max-connections and Update JSON Unmarshaller