Skip to content
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

Closed
wants to merge 23 commits into from
Closed

Decouple admin server from plugins startup, add proxy status endpoint #181

wants to merge 23 commits into from

Conversation

kvosper
Copy link
Contributor

@kvosper kvosper commented Jun 11, 2018

No description provided.

*
* @param <T> type of success value
*/
public class Result<T> {
Copy link
Contributor

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

Copy link
Contributor Author

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() {
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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();
});
Copy link
Contributor

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?

Copy link
Contributor Author

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");
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor

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));
});
Copy link
Contributor

@mikkokar mikkokar Jun 12, 2018

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@mikkokar
Copy link
Contributor

The Travis build has started failing. I assume you are still working on this PR?

There are three failing e2e tests in OriginsReloadRepetitionSpec and OriginsReloadSpec test suites. All of them show similar symptoms:

�[31m  The code passed to eventually never returned normally. Attempted 60 times over 1.00299402 seconds. Last failure message: "{"appOne":{"appId":"appOne","activeOrigins":[],"inactiveOrigins":[],"disabledOrigins":[]}}" did not include substring "localhost:41693". (OriginsReloadSpec.scala:73)�[0m
�[31m- should reflect origin removal in origins status page *** FAILED ***�[0m

That is, no origins are shown in the origin status response.

@kvosper
Copy link
Contributor Author

kvosper commented Jun 14, 2018

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 OriginsReloadSpec to see if it makes a difference. I will likely also add extra logging as I cannot see any difference in the logs when the tests pass compared to these failures.

Update for my update - increasing the timeout for should reflect origin addition in origins status page from 1 second to 10 seconds did not prevent it from failing.

@kvosper
Copy link
Contributor Author

kvosper commented Jun 15, 2018

Looks like the test OriginsReloadSpec causes two OriginsSnapshots to be sent when it changes the backends. First to remove the old origins, then to add the new ones.
For some reason, on travis, the snapshot from the removal is being received later than the snapshot from the addition, i.e. out of order.

@kvosper
Copy link
Contributor Author

kvosper commented Jun 25, 2018

This is being postpone until required improvements are made separately.

@kvosper
Copy link
Contributor Author

kvosper commented Aug 6, 2018

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.

@kvosper kvosper closed this Aug 6, 2018
@kvosper kvosper deleted the server-started-endpoint branch August 24, 2018 16:31
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.

None yet

3 participants