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

New entry-point: Dialogue.newClientPool() #398

Closed
wants to merge 22 commits into from
Closed

Conversation

iamdanfox
Copy link
Contributor

@iamdanfox iamdanfox commented Feb 20, 2020

Before this PR

  • We need to give people an easy way to build multiple clients that all use the same connection pool under the hood.
  • There's quite a few of different little bits of API surface in dialogue, which makes it a little confusing for users to know what they're supposed to interact with
  • We don't currently cleanup or dispose of client resources when we're done
  • We also rely on the old c-j-r ClientConfiguration type everywhere

After this PR

==COMMIT_MSG==
A new Dialogue.newClientPool() method gives people a single clear entrypoint to the library.
==COMMIT_MSG==

It people build clients in a declarative way that people are familiar with from feign:

AsyncFooService foo = clients.get(AsyncFooService.class, listenableConfig);
AsyncOtherService other = clients.get(AsyncOtherService.class, listenableConfig);

// ... later, wc can call:
clients.close()

By getting conjure to generate enums, we can actually do this without using any sketchy reflection (no InvocationTargetExceptions anywhere!)

Obviously this doesn't have enough tests to merge yet, just wanted to see what you guys think of the idea before I sink too much time into it!

Possible downsides?

  • yet another refreshable implementation

* Facilitates creating many clients which all share the same connection pool. Should only create one of these per
* server. Close it when your server shuts down to release resources.
*/
public interface ClientPool extends Closeable {
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 probably a reasonable place to start reading this PR if you want

import com.google.errorprone.annotations.MustBeClosed;
import com.palantir.dialogue.ConjureRuntime;

public final class Dialogue {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

My dream is if we can make all the things people care about accessible from one top-level starting point, so they can just remember "the method is on Dialogue somewhere".

Copy link
Contributor

Choose a reason for hiding this comment

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

I think if that's the case it'd be better to return a ClientFactory that uses the provided ClientPool so that no user of Dialogue has to know anything about something called a ClientPool.

Copy link
Contributor

Choose a reason for hiding this comment

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

it's not so clear to me that this code should live in dialogue land rather than in wc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@uschi2000 are you referring to just this one entrypoint or the whole PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the idea of a "singleton client factory", i.e., something that holds on to resources for the lifetime of a process, is something I'd expect in a server library, not a client library.

Copy link
Contributor Author

@iamdanfox iamdanfox Feb 21, 2020

Choose a reason for hiding this comment

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

I think if we try to make all our libraries good enough to use on their own, then when we pull them into WC they'll still be good. I actually think a client library that doesn't give you a convenient way to release resources when you want to seems broken/incomplete.

To be clear this API doesn't know anything about the lifetime of a process, and there are no static singletons here. it would still be up to witchcraft to make sure there's one client pool and witchcrafts responsibility to close it at shutdown time.

Copy link
Contributor

Choose a reason for hiding this comment

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

Dialogue is designed to be a singleton, no?
Yes, it's a bug today that we don't close channels, but then why isn't it sufficient for this library to add Channel#close? I prefer hierarchical ownership (with delegating .close()) over complex "inside-out" resource management; is that not an option here?

@markelliot
Copy link
Contributor

markelliot commented Feb 20, 2020

We need to give people an easy way to build multiple clients that all use the same connection pool under the hood.

Is this actually desirable?

* will always fail if the specified uri is not listed in the latest version of the config. Somewhat dangerous
* because this has no limits / failover.
*/
Channel rawHttpChannel(String uri, Listenable<DialogueConfig> config);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this actually something that should be exposed?

Copy link
Contributor Author

@iamdanfox iamdanfox Feb 21, 2020

Choose a reason for hiding this comment

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

Probably not to be honest - I started with this 3-member interface just to hash out in my head what the key building blocks were. Happy to push it down to package private.

* Returns a channel for interacting with the given abstract upstream service, which routes traffic
* appropriately to the various available nodes. Live-reloaded every time the urls change.
*/
Channel smartChannel(Listenable<DialogueConfig> config);
Copy link
Contributor

Choose a reason for hiding this comment

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

when does one use this instead of get?

@iamdanfox
Copy link
Contributor Author

I think so, because the average WC server has 38 clients according to datadog, but I think some have much fewer (like maybe 2) and some have even more. This means any limits are gonna seem to apply quite differently. It also tries to provide similar-ish behaviour to our old c-j-r okhttp world, where there was just one static instance.

From the metric summary page in datadog (which shows you tag cardinality), we've got 26.7k witchcraft servers (reporting the jvm.safepoint.time metric) and 1,016,011 series of client.response.count. 1016011 / 26700 = 38

.getOrComputeIfAbsent(
"one-off-client-construction",
unused -> {
return constructLiveReloadingClient(config); // we'll re-use this instance every time
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need a unique CloseableHttpClient instance for each ssl configuration -- each service can have different trust data. A few other prefs from the configuration are applied directly to the CloseableHttpClient so they will require unique instances

* Returns a channel for interacting with the given abstract upstream service, which routes traffic
* appropriately to the various available nodes. Live-reloaded every time the urls change.
*/
Channel smartChannel(Listenable<DialogueConfig> config);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should have some API separation between channel creation and using a channel to create a generated client (which would delegate to the first component)

return factory.construct(smartChannel, runtime);
}

private static <F> F getOnlyEnumConstant(Class<F> factoryClass) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private static <F> F getOnlyEnumConstant(Class<F> factoryClass) {
private static <F extends Enum<F>> F getOnlyEnumConstant(Class<F> factoryClass) {

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 only way I can get this to work is if I then make the argument Class<?>, which seems a bit less nice?

image


Class<? extends HttpChannelFactory> getFactoryClass() {
try {
return (Class<? extends HttpChannelFactory>) Class.forName(className);
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need reflection here?

Copy link
Contributor

Choose a reason for hiding this comment

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

i.e., why not Supplier<Channel>?

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 didn't want people who depend on dialogue-core to transitively get dialogue-apache, dialogue-java, dialogue-okhttp etc.

Might be able to doing this with a compileOnly dependency if you'd prefer?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a huge fan of runtime dependencies like this. might indicate that the decomp is a little off, and maybe also that this should live in the server repo?

@iamdanfox
Copy link
Contributor Author

Closing out as I'm resuscitating my PR to do this in WC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants