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
JAMES-2813 Add non-polling and timeout on TaskManager.await #2653
Conversation
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found") | ||
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The taskId is invalid or invalid timeout"), | ||
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found"), | ||
@ApiResponse(code = HttpStatus.REQUEST_TIMEOUT_408, message = "The timeout have been reached") |
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.
s/have/has
ErrorResponder.builder() | ||
.statusCode(HttpStatus.REQUEST_TIMEOUT_408) | ||
.type(ErrorResponder.ErrorType.SERVER_ERROR) | ||
.message("The timeout have been reached") |
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.
s/have/has
.queryParam("timeout", "5") | ||
.when() | ||
.get("/" + taskId.getValue() + "/await") | ||
.then() |
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.
Indent
|
||
given() | ||
.queryParam("timeout", "-5") | ||
.when() |
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.
Indent
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.
That went back somehow in one of your fixup?
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.
a github bug I think
.queryParam("timeout", "-5") | ||
.when() | ||
.get("/" + taskId.getValue() + "/await") | ||
.then() |
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.
Indent
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.
That went back somehow in one of your fixup?
|
||
given() | ||
.queryParam("timeout", "2") | ||
.when() |
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.
Indent
.queryParam("timeout", "2") | ||
.when() | ||
.get("/" + taskId.getValue() + "/await") | ||
.then() |
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.
Indent
void getAwaitWithANonExistingTaskShouldReturnNotFound() { | ||
when() | ||
.get("/" + TaskId.generateTaskId().asString() + "/await") | ||
.then() |
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.
Indent
} | ||
|
||
@Test | ||
default void awaitANonExistingTaskShouldReturnAUnknownAwaitedTaskExecutionDetailsAndThrow() { |
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.
s/AUnknown/AnUnknown
|
235366e
to
a00bcb5
Compare
.orElse(Long.MAX_VALUE / 1000); | ||
|
||
Preconditions.checkState(value > 0); | ||
return Duration.ofSeconds(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.
why do you divide by 1000 and the take a Duration.ofSeconds? You could directly take a Duration.ofMillis, no?
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.
ok I got the explanation, but the readability is not very clear. If you name Long.MAX_VALUE as a constant, it would be better.
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 unit in the API is seconds
and the maximal value of Duration
is Duration.ofMillis(Long.MAX_VALUE)
, I'm forced to divide to avoid overflows.
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.
Who will need to wait an HTTP request for 106751991167300 days? What if you assert a max value of 1 day for example?
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.
BTW, there's no test to check this "feature"
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.
TasksRoutesTests.getAwaitWithInvalidTimeoutShouldReturnAnError()
I prefer let the user the choice
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.
It doesn't check that you can put 106751991167300 days
@@ -3269,16 +3269,17 @@ One can await the end of a task, then receive it's final execution report. | |||
That feature is especially usefull for testing purpose but still can serve real-life scenari. | |||
|
|||
``` | |||
curl -XGET http://ip:port/tasks/3294a976-ce63-491e-bd52-1b6f465ed7a2/await | |||
curl -XGET http://ip:port/tasks/3294a976-ce63-491e-bd52-1b6f465ed7a2/await?timeout=numberOfSeconds |
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.
as you divide by 1000 it's millis, no?
BTW what about allowing some timeunit here? I guess we already have all the needed available code for that.
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've found nothing regarding that.
Anyway, I think it clearly not worth the time and effort maintaining such code and a decent suite of tests.
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 that new timeout parameter now compulsory?
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.
org.apache.james.util.DurationParser?
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 that new timeout parameter now compulsory?
no
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.
org.apache.james.util.DurationParser?
spaces are mandatory, it look likes a bad idea
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.
that's not how I read it. But maybe some missing tests
|
a00bcb5
to
7e4f80a
Compare
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found") | ||
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The taskId is invalid or invalid timeout"), | ||
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found"), | ||
@ApiResponse(code = HttpStatus.REQUEST_TIMEOUT_408, message = "The timeout have been reached") |
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.
do you think the message add anything?
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.
sure, because it specifies the cause of the request timeout
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.
do you know other timeout cause than "The timeout has been reached"?
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 server has reached it's life timeout
The user should have reach her end of life
More seriously I'm not sure it means exactly what it should (see https://tools.ietf.org/html/rfc7231#section-6.5.7 ), but I'm fine with it anyway.
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.
can the result when reached timeout be represented in by 200 response code and a Json body telling that the task is in processing? and the message will be more clear than just a status code
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.
A 200 status code for an exceeding timeout, I disagree, we submit a request, which is the specification of a process, we can't fulfill the requirements, we should not say that it's ok.
.orElse(Long.MAX_VALUE / 1000); | ||
|
||
Preconditions.checkState(value > 0); | ||
return Duration.ofSeconds(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.
Who will need to wait an HTTP request for 106751991167300 days? What if you assert a max value of 1 day for example?
.orElse(Long.MAX_VALUE / 1000); | ||
|
||
Preconditions.checkState(value > 0); | ||
return Duration.ofSeconds(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.
BTW, there's no test to check this "feature"
new TerminatedAwaitedTaskExecutionDetails(getExecutionDetails(id)) | ||
} | ||
} catch { | ||
case _: IllegalStateException => new TimeoutAwaitedTaskExecutionDetails() |
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.
do you need this _: Foo
syntax ?
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 so
|
7e4f80a
to
9e0b888
Compare
9e0b888
to
e8e5929
Compare
|
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found") | ||
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The taskId is invalid or invalid timeout"), | ||
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The taskId is not found"), | ||
@ApiResponse(code = HttpStatus.REQUEST_TIMEOUT_408, message = "The timeout have been reached") |
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.
can the result when reached timeout be represented in by 200 response code and a Json body telling that the task is in processing? and the message will be more clear than just a status code
} else { | ||
Flux.from(terminationSubscriber.listenEvents) | ||
.filter{ | ||
override final def await(id: TaskId, timeout: Duration): AwaitedTaskExecutionDetails = { |
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.
don't you think Optional<Duration>
could be better?
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 the submit()
need to support timeout too
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.
No, because you would not not if you had an Option.empty
due to an TaskNotFound
or a TimeOut
.
You want it to fail if you can not submit a Task.
d061946
to
49f6576
Compare
test this please |
.get("/" + taskId.getValue() + "/await") | ||
.then() | ||
.statusCode(HttpStatus.BAD_REQUEST_400) | ||
.body("message", is("Invalid timeout")); |
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.
Why? (details?)
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.
which details?
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.
Why is the timeout invalid? Can you add it in the returned exception detailed field?
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.
you pass one parameter, it is invalid, what would be the gain?
}); | ||
|
||
given() | ||
.queryParam("timeout", "2") |
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.
Can't we reduce it to 1s? 100ms? (faster tests?)
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.
You won't gained much
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.
1 to 2 second a test?
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.
1 to 2 second a test?
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.
on 3 tests, we will survive
@Test | ||
void getAwaitWithANonExistingTaskShouldReturnNotFound() { | ||
when() | ||
.get("/" + TaskId.generateTaskId().asString() + "/await") |
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.
Extract that variable to unknownTaskId
?
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.
cosmetic, I'll fix it if I have other modifications
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.
not cosmetic IMO
try { | ||
byte[] payload = serializer.serialize(event).getBytes(StandardCharsets.UTF_8); | ||
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties.Builder().build(); | ||
OutboundMessage message = new OutboundMessage(EXCHANGE_NAME, ROUTING_KEY, basicProperties, payload); |
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 see that Event => OutboundMessage
serialization a bit everywhere. Wouldn't it make sens to factorize it?
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.
Not really, we have multiple ways to serialize them
.share(); | ||
} | ||
|
||
private Mono<Event> toEvent(Delivery delivery) { |
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.
Idem for that Delivery => Event
.
} | ||
|
||
public TaskExecutionDetails unwrap() { | ||
throw new RuntimeException("await has failed due to timeout"); |
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.
Can't we use a more specific exception?
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.
no, it's a call issue, it's not supposed to happen in production
} | ||
|
||
public TaskExecutionDetails unwrap() { | ||
throw new RuntimeException("await has failed due to unknown task"); |
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.
Can we use a more specific exception?
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.
idem
@@ -58,6 +60,62 @@ public boolean isFinished() { | |||
} | |||
} | |||
|
|||
interface AwaitedTaskExecutionDetails { |
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.
Isn't this a disguised sketchy staged builder?
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.
A staged builder of one element? it will only make it heavier for no benefit
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.
Well I have the impression that you return several time invalid variations of the same entity, for which calling unwrap fails with a not specific exception.
You require IMO too much knowledge from the caller (if I use it, I will break it) and I fail to understand the way this "thing" - that do not follow patterns that I know - work.
Can we try to review and rediscuss code organization here?
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.
No, and I won't fix it.
- You'll loose in expressiveness (have a look in at tests)
- You'll add burden to code which is already complex adding no value
- You have to test the code you produce (including using this one), so you'll be corrected quickly
- It's perfectly acceptable to have code not behaving the way we are use to, that does not make it a bad code (rephrasing: it's not because we know things that work, that there is nothing we do not know that work too)
|
||
assertThatCode(() -> result | ||
.onTimeout(CustomTimeoutException::new) | ||
.onUnknown(CustomUnknownException::new) |
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.
Why is it the responsability of the caller to instanciate the exceptions?
Why can't we have well defined exceptions thrown directly by the await
method?
Sorry I don't get the benefits behind all of this complexity...
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.
Why is it the responsability of the caller to instanciate the exceptions?
I do now instantiate it here, I pass the method reference
Why can't we have well defined exceptions thrown directly by the await method?
Because reaching a timeout is not a real exception, consequently, it should not be manage like that
Sorry I don't get the benefits behind all of this complexity...
ADT are not complex, RunTimeException
s are
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 do now instantiate it here, I pass the method reference
Thanks I understand
Because reaching a timeout is not a real exception, consequently, it should not be manage like that
Thanks that's a beginning of answer
ADT are not complex, RunTimeExceptions are
Who said that it should be runtime?
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.
Who said that it should be runtime?
Well, if it's not passed as a value, the only other non too-dirty way I see is an Exception
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.
That would be more straightforward, no?
9f86816
to
ae5ca97
Compare
@@ -223,7 +225,7 @@ void getAwaitWithInvalidTimeoutShouldReturnAnError() { | |||
.get("/" + taskId.getValue() + "/await") | |||
.then() | |||
.statusCode(HttpStatus.BAD_REQUEST_400) | |||
.body("message", is("Invalid timeout")); | |||
.body("message", allOf(containsString("Invalid timeout"), containsString("Timeout should not be positive"))); |
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.
Wait... Shouldn't it be Timeout should be positive
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.
Once the typo in the webadmin error message is fixed
test this please |
@@ -196,7 +196,7 @@ private Duration getTimeout(Request req) { | |||
.filter(parameter -> !parameter.isEmpty()); | |||
|
|||
requestTimeout.ifPresent(timeout -> | |||
Preconditions.checkState(!timeout.replaceAll(" ", "").startsWith("-"), "Timeout should not be positive")); | |||
Preconditions.checkState(!timeout.replaceAll(" ", "").startsWith("-"), "Timeout should be positive")); |
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.
Checking the first character is a strange way of ensuring a number is negative...
(Sorry, I missed it so far)
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.
It's the only way I've founded to check it, since an Integer.parseXXX
would not recognize the unit and duration parsing won't recognize the minus sign, throwing a generic error
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.
Let me have a look...
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.
See 2f10137
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.
Thank you
.map(rawString -> DurationParser.parse(rawString, ChronoUnit.SECONDS)) | ||
.orElse(MAXIMUM_AWAIT_TIMEOUT); | ||
|
||
Preconditions.checkState(timeout.compareTo(MAXIMUM_AWAIT_TIMEOUT) <= 0, "Timeout should not exceed one year"); |
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.
A precondition at the end of a method look weird. Moving it to a ensureXxx method would solve this.
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 did not find an example, can you give me one please?
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.
See See 2f10137
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.
Thank you
Overwise 👍 |
tests were 🍏
test this please |
test this please |
1 similar comment
test this please |
test this please |
} | ||
|
||
private void assertDoesNotExceedMaximumTimeout(Duration timeout) { | ||
Preconditions.checkState(timeout.compareTo(MAXIMUM_AWAIT_TIMEOUT) <= 0, "Timeout should not exceed one year"); |
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.
s/one year/365 days/
@@ -58,6 +59,9 @@ public boolean isFinished() { | |||
} | |||
} | |||
|
|||
class ReachedTimeoutException extends Exception { |
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.
why the hell a checked Exception?
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.
to force handling it
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.
usually we use a RuntimeException for these cases, and explicitly state that the method is throwing it with a throws MyRuntimeException
. What's the point here to force all the callers to handle it?
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.
We forgot to handle it at the right places, it would be a shame to allow this kind of bug to happen again. It's a design fix.
|
can be considered 🍏 |
This enables a nicer error message upon negative numbers, avoiding hacks on the caller side.
f2aaa59
to
0585eac
Compare
test this please |
test this please |
} | ||
|
||
@Test | ||
default void awaitShouldAwaitWaitingTask() { | ||
default void awaitShouldAwaitWaitingTask() throws TaskManager.ReachedTimeoutException, InterruptedException { |
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.
throws Exception is sufficient for a test
@@ -58,6 +59,9 @@ public boolean isFinished() { | |||
} | |||
} | |||
|
|||
class ReachedTimeoutException extends Exception { |
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.
usually we use a RuntimeException for these cases, and explicitly state that the method is throwing it with a throws MyRuntimeException
. What's the point here to force all the callers to handle it?
Merged |
No description provided.