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
Decouple admin server from plugins startup, add proxy status endpoint #181
Conversation
* | ||
* @param <T> type of success value | ||
*/ | ||
public class Result<T> { |
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 haven't checked the code that uses this class or thought about it considering we are doing Java, but I believe we wanted to implement this as an Either/Try monad? So I would expect : map, flatMap... Scala's try monad is nicer (as in simpler) https://mauricio.github.io/2014/02/17/scala-either-try-and-the-m-word.html , but it seems there are a few implementations in Java such as https://github.com/jasongoodwin/better-java-monads/blob/master/src/main/java/com/jasongoodwin/monads/Try.java
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 goal of this class was simply to add a representation of success or failure that can be stored as a value.
I can see a similarity with the Try.java
linked, but I'm not sure it is worth adding a new dependency just for this class.
In terms of API, a difference I see is that the Result
class can be instantiated directly with a call to success
or failure
without needing to wrap a block of code, so it is in that sense more decoupled from the try-catch structure and can be used in situations where try-catch does not appear (e.g. using result values in unit tests).
} | ||
|
||
@Test | ||
public void valueInConfigStoreIsConsistentWithListenersUpdate() { |
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 test can be folded into the previous one (listenersReceiveUpdatesWhenValuesChange) by adding one line at the end:
assertThat(configStore.get("foo"), is(of("bar")));
That is:
@Test
public void valueInConfigStoreIsConsistentWithListenersUpdate() {
AtomicReference<String> update = new AtomicReference<>();
Latch sync = new Latch(1);
configStore.watch("foo", String.class)
.subscribe(value -> {
update.set(value);
sync.countDown();
});
configStore.set("foo", "bar");
sync.await(1, SECONDS);
assertThat(update.get(), is("bar"));
assertThat(configStore.get("foo"), is(of("bar")));
}
|
||
configStore.set("foo", "bar"); | ||
unlockedByTestThread.countDown(); | ||
unlockedBySubscribeThread.await(2, SECONDS); |
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 you clarify the purpose of this test please? Is it to show that the watch subscriptions run from a different thread from the config setter thread?
If so, does every watch subscriber run in their own thread? How far will it scale? Say I have say 10 watch subscribes on "foo", then on setting "foo" do they run simultaneously on 10 different threads? Or are they queued to a thread pool?
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 it to show that the watch subscriptions run from a different thread from the config setter thread?
Yes.
If so, does every watch subscriber run in their own thread? How far will it scale? Say I have say 10 watch subscribes on "foo", then on setting "foo" do they run simultaneously on 10 different threads? Or are they queued to a thread pool?
That code is already part of this PR. You can look at the watch
method in the ConfigStore
class. Please review.
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 this was just to probe your intentions regarding config store threading model. I think it is ok for now. Perhaps in future we may want to allow consumers to provide a custom executor.
.subscribe(value -> { | ||
unlockedByTestThread.await(1, SECONDS); | ||
unlockedBySubscribeThread.countDown(); | ||
}); |
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.
When registering a watch, a possibly existing value should be propagated straight away. This prevents unnecessary race conditions where a consumer is late to observe the event.
Please confirm if the watch
behaves like 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.
This is not currently the case, I will fix it.
.map(Result::successValue) | ||
.filter(Optional::isPresent) | ||
.map(Optional::get) | ||
.orElse("INCOMPLETE"); |
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 wouldn't store anything complicated in the "server.started.proxy" attribute. Please consider just storing an enum of STARTED
, FAILED
or INCOMPLETE
instead of Result
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, a caused exception is stored in the result object (failure(cause)
) in NettyServer
java, just to be thrown away from the server status response.
Therefore consider removing Result
class.
The detailed error cause can be logged.
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 it is going to eliminate the above 6 or 7 lines of code :-)
routesForPlugins(plugins).forEach(route -> httpRouter.add(route.path(), route.handler())); | ||
httpRouter.add("/admin/tasks/plugin/", new PluginToggleHandler(plugins)); | ||
httpRouter.add("/admin/plugins", new PluginListHandler(plugins)); | ||
}); |
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 don't necessarily need a watch functionality for this use case. You can just render the admin links in handle()
method every time a request comes in. This way you'll just query the config store and add the links to the end of the page.
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 code does not render links, it builds the HttpRouter.
} | ||
} | ||
|
||
private static void verifyThatLinksInIndexMapToRealEndpoints(Iterable<IndexHandler.Link> links, Set<String> paths) { |
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 something we should have in the unit/e2e tests, rather than in the running code?
What would be the suggested remedial action for the end user if this exception ever fires?
.build(); | ||
} | ||
|
||
private HttpResponse.Builder addInfoHeader(HttpResponse.Builder responseBuilder, HttpRequest request) { | ||
return responseBuilder.header(styxInfoHeaderName, responseInfoFormat.format(request)); | ||
} | ||
|
||
public ProxyServerBuilder httpHandler(HttpHandler2 httpHandler) { | ||
public ProxyServerBuilder httpHandler(Supplier<HttpHandler2> httpHandler) { | ||
this.httpHandler = httpHandler; | ||
return 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.
Can you remind me why the httpHnadler
has to be loaded lazily? To prevent a blocking load of plugins before the admin interface starts?
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.
Yes that is the reason.
@@ -66,18 +66,20 @@ private static void initialisePlugins(Iterable<NamedPlugin> plugins) { | |||
if (exceptions > 0) { | |||
throw new RuntimeException(format("%s plugins failed to start", exceptions)); | |||
} | |||
|
|||
configStore.set("plugins", plugins); |
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.
Ideally this would move inside the loop:
for (NamedPlugin plugin : plugins) {
try {
plugin.styxStarting();
configStore.set("plugins", pluginsInitialisedSoFar); // <----
} catch (Exception e) {
exceptions++;
LOG.error("Error starting plugin '{}'", plugin.name(), e);
}
}
This would give a more instantaneous feedback about the plugins loaded so far. The end user could monitor the progress of plugins being initialised. Maybe something to consider in future.
However the current implementation is fine for now.
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 had the same thought, but for now the current implementation is simpler.
The Travis build has started failing. I assume you are still working on this PR? There are three failing e2e tests in
That is, no origins are shown in the origin status response. |
An update on failures - I cannot reproduce this failure locally, making it difficult to diagnose. I am going to try increasing timeouts on the tests in Update for my update - increasing the timeout for |
Looks like the test |
This is being postpone until required improvements are made separately. |
Closing as this is dependent on other work, and the codebase has changed since it was opened. It will probably be done after Styx 1.0 is released. |
No description provided.