Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import com.palantir.dialogue.core.DialogueChannel;
import com.palantir.dialogue.core.DialogueDnsResolver;
import com.palantir.dialogue.core.StickyEndpointChannels;
import com.palantir.dialogue.core.StickyEndpointChannelsFactory;
import com.palantir.dialogue.core.TargetUri;
import com.palantir.dialogue.hc5.ApacheHttpClientChannels;
import com.palantir.logsafe.Preconditions;
Expand Down Expand Up @@ -300,7 +301,7 @@ public StickyChannelFactory2 getStickyChannels2(String serviceName) {
return new StickyChannelFactory2() {
@Override
public Channel getStickyChannel() {
return internalDialogueChannel.get().stickyChannels().get();
return internalDialogueChannel.get().stickyChannel();
}

@Override
Expand Down Expand Up @@ -504,9 +505,10 @@ public String toString() {
+ exceptionSupplier.get().getMessage() + '}';
}

@Unsafe
@Override
public Supplier<Channel> stickyChannels() {
return () -> this;
public Channel stickyChannel() {
return this;
}

@Override
Expand All @@ -516,9 +518,7 @@ public EndpointChannel endpoint(Endpoint _endpoint) {
}

/* Abstracts away DialogueChannel so that we can handle no-service/no-uri case in #getInternalDialogueChannel. */
private interface InternalDialogueChannel extends Channel, EndpointChannelFactory {
Supplier<Channel> stickyChannels();
}
private interface InternalDialogueChannel extends Channel, EndpointChannelFactory, StickyEndpointChannelsFactory {}

private static final class InternalDialogueChannelFromDialogueChannel implements InternalDialogueChannel {

Expand All @@ -539,8 +539,8 @@ public EndpointChannel endpoint(Endpoint endpoint) {
}

@Override
public Supplier<Channel> stickyChannels() {
return dialogueChannel.stickyChannels();
public Channel stickyChannel() {
return dialogueChannel.stickyChannel();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.dialogue.core;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.palantir.dialogue.Endpoint;
import com.palantir.dialogue.EndpointChannel;
import com.palantir.dialogue.EndpointChannelFactory;

final class CachingEndpointChannelFactory implements EndpointChannelFactory {

private final LoadingCache<Endpoint, EndpointChannel> cache;
private final EndpointChannelFactory delegate;

CachingEndpointChannelFactory(EndpointChannelFactory delegate, int maxEndpointCacheSize) {
this.delegate = delegate;
this.cache = Caffeine.newBuilder()
.maximumSize(maxEndpointCacheSize)
.weakValues()
.build(delegate::endpoint);
Comment on lines +32 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

side-note: it may be worth starting to use https://github.com/palantir/cache for new caches like this (which iirc gives us instrumentation by default, which could be interesting to have regardless, since that would help identify cases where the cache size may be inappropriate)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That library doesn't support weak values (and I'm not sure I want to support weak values there). Using Caffeine feels more appropriate here.

}

@Override
public EndpointChannel endpoint(Endpoint endpoint) {
return cache.get(endpoint);
}

@Override
public String toString() {
return "CachingEndpointChannelFactory{" + delegate + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ default int maxQueueSize() {
return 100_000;
}

/** Maximum number of cached endpoint channels. Values {@code <= 0} disable caching. */
@Value.Default
default int maxEndpointCacheSize() {
Comment on lines +82 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to be specific to sticky channels, but the javadoc doesn't surface that at all. cached endpoint channels is instead pretty generic. It may be worth adding some clarification.

Additionally, I don't think users can modify this config directly, so we may want to pipe it through DialogueChannel.Builder

return 1_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thoughts on starting with this at 0 to keep parity with existing logic, and testing a different value in some prod environment, before changing the default?

I also wonder whether 1000 is enough/too much.

}

OptionalInt overrideSingleHostIndex();

@Value.Default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.palantir.dialogue.EndpointChannelFactory;
import com.palantir.dialogue.Request;
import com.palantir.dialogue.Response;
import com.palantir.dialogue.core.DialogueChannelFactory.ChannelArgs;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.Safe;
import com.palantir.logsafe.SafeArg;
Expand All @@ -49,16 +50,17 @@
import java.util.function.Function;
import java.util.function.Supplier;

public final class DialogueChannel implements Channel, EndpointChannelFactory {
public final class DialogueChannel implements Channel, EndpointChannelFactory, StickyEndpointChannelsFactory {
private static final SafeLogger log = SafeLoggerFactory.get(DialogueChannel.class);
private final EndpointChannelFactory delegate;
private final Config cf;
private final Supplier<Channel> stickyChannelSupplier;
private final StickyEndpointChannelsFactory stickyEndpointChannelsFactory;

private DialogueChannel(Config cf, EndpointChannelFactory delegate, Supplier<Channel> stickyChannelSupplier) {
private DialogueChannel(
Config cf, EndpointChannelFactory delegate, StickyEndpointChannelsFactory stickyEndpointChannelsFactory) {
this.cf = cf;
this.delegate = delegate;
this.stickyChannelSupplier = stickyChannelSupplier;
this.stickyEndpointChannelsFactory = stickyEndpointChannelsFactory;
}

@Override
Expand All @@ -71,8 +73,17 @@ public EndpointChannel endpoint(Endpoint endpoint) {
return delegate.endpoint(endpoint);
}

/**
* @deprecated use {@link #stickyChannel()}
*/
@Deprecated
public Supplier<Channel> stickyChannels() {
return stickyChannelSupplier;
return () -> stickyChannel();
}

@Override
public Channel stickyChannel() {
return stickyEndpointChannelsFactory.stickyChannel();
}

public static Builder builder() {
Expand Down Expand Up @@ -219,8 +230,7 @@ public LimitedChannel apply(List<TargetUri> targetUris) {

Channel multiHostQueuedChannel = QueuedChannel.create(cf, stickyValidationChannel);
EndpointChannelFactory channelFactory = createEndpointChannelFactory(multiHostQueuedChannel, cf);

Supplier<Channel> stickyChannelSupplier =
StickyEndpointChannelsFactory stickyEndpointChannelsFactory =
StickyEndpointChannels2.create(cf, stickyValidationChannel, channelFactory);

Meter createMeter = clientMetrics
Expand All @@ -230,7 +240,7 @@ public LimitedChannel apply(List<TargetUri> targetUris) {
.build();
createMeter.mark();

return new DialogueChannel(cf, channelFactory, stickyChannelSupplier);
return new DialogueChannel(cf, channelFactory, stickyEndpointChannelsFactory);
}

private static ImmutableList<LimitedChannel> createHostChannels(
Expand All @@ -241,7 +251,7 @@ private static ImmutableList<LimitedChannel> createHostChannels(
cf.overrideSingleHostIndex().orElse(uriIndex);
TargetUri targetUri = targetUris.get(uriIndex);
Channel channel = cf.channelFactory()
.create(DialogueChannelFactory.ChannelArgs.builder()
.create(ChannelArgs.builder()
.uri(targetUri.uri())
.uriIndexForInstrumentation(uriIndexForInstrumentation)
.resolvedAddress(targetUri.resolvedAddress())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,34 @@
import javax.annotation.concurrent.ThreadSafe;
import org.jspecify.annotations.Nullable;

final class StickyEndpointChannels2 implements Supplier<Channel> {
final class StickyEndpointChannels2 implements StickyEndpointChannelsFactory {

private final Supplier<EndpointChannelFactory> delegate;
private final Supplier<Channel> queueOverrideSupplier;
private final EndpointChannelFactory delegate;

StickyEndpointChannels2(Supplier<EndpointChannelFactory> endpointChannelFactory) {
StickyEndpointChannels2(Supplier<Channel> queueOverrideSupplier, EndpointChannelFactory endpointChannelFactory) {
this.queueOverrideSupplier = queueOverrideSupplier;
this.delegate = endpointChannelFactory;
}

@Override
public Channel get() {
return new StickyChannel2(delegate.get());
public Channel stickyChannel() {
return new StickyChannel2(queueOverrideSupplier, delegate);
}

@Override
public String toString() {
return "StickyEndpointChannels2{" + delegate + "}";
}

static Supplier<Channel> create(Config cf, LimitedChannel nodeSelectionChannel, EndpointChannelFactory delegate) {
static StickyEndpointChannelsFactory create(
Config cf, LimitedChannel nodeSelectionChannel, EndpointChannelFactory delegate) {
Supplier<Channel> queueOverrideSupplier = new QueueOverrideSupplier(cf, nodeSelectionChannel);
return new StickyEndpointChannels2(
new StickyEndpointChannels2EndpointFactorySupplier(queueOverrideSupplier, delegate));
int maxEndpointCacheSize = cf.maxEndpointCacheSize();
EndpointChannelFactory channelFactory = (maxEndpointCacheSize <= 0)
? delegate
: new CachingEndpointChannelFactory(delegate, maxEndpointCacheSize);
Comment on lines +59 to +62

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This feels a bit like overkill to me.

If we want to keep this, I'd consider making the CachingEndpointChannelFactory constructor private and pushing this logic into a static factory method there, so callers don't need to be concerned with this.

return new StickyEndpointChannels2(queueOverrideSupplier, channelFactory);
}

private static final class QueueOverrideSupplier implements Supplier<Channel> {
Expand All @@ -81,43 +87,25 @@ public Channel get() {
}
}

private static final class StickyEndpointChannels2EndpointFactorySupplier
implements Supplier<EndpointChannelFactory> {

private final Supplier<Channel> queueOverrideSupplier;
private final EndpointChannelFactory delegate;

StickyEndpointChannels2EndpointFactorySupplier(
Supplier<Channel> queueOverrideSupplier, EndpointChannelFactory delegate) {
this.queueOverrideSupplier = queueOverrideSupplier;
this.delegate = delegate;
}

@Override
public EndpointChannelFactory get() {
Channel queueOverride = queueOverrideSupplier.get();
return endpoint -> {
EndpointChannel endpointChannel = delegate.endpoint(endpoint);
return (EndpointChannel) request -> {
QueueAttachments.setQueueOverride(request, queueOverride);
return endpointChannel.execute(request);
};
};
}
}

private static final class StickyChannel2 implements EndpointChannelFactory, Channel {
private static final class StickyChannel2 implements Channel, EndpointChannelFactory {

private final Channel queueOverride;
private final EndpointChannelFactory channelFactory;
private final StickyRouter router = new StickyRouter();

private StickyChannel2(EndpointChannelFactory channelFactory) {
private StickyChannel2(Supplier<Channel> queueOverrideSupplier, EndpointChannelFactory channelFactory) {
this.queueOverride = queueOverrideSupplier.get();
this.channelFactory = channelFactory;
}

@Override
public EndpointChannel endpoint(Endpoint endpoint) {
return new StickyEndpointChannel(router, channelFactory.endpoint(endpoint));
EndpointChannel endpointChannel = channelFactory.endpoint(endpoint);
EndpointChannel endpointWithQueueOverride = innerRequest -> {
QueueAttachments.setQueueOverride(innerRequest, queueOverride);
return endpointChannel.execute(innerRequest);
};
return request -> router.execute(request, endpointWithQueueOverride);
}

@Override
Expand Down Expand Up @@ -240,24 +228,4 @@ private static ListenableFuture<Response> executeWithStickyTarget(
return endpointChannel.execute(request);
}
}

private static final class StickyEndpointChannel implements EndpointChannel {

@pkoenig10 pkoenig10 May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Can we keep this? Having non-anonymous classes is helpful for telemetry/debugging because it has a nice name in stacktraces and has a toString implementation.

private final StickyRouter stickyRouter;
private final EndpointChannel delegate;

StickyEndpointChannel(StickyRouter stickyRouter, EndpointChannel delegate) {
this.stickyRouter = stickyRouter;
this.delegate = delegate;
}

@Override
public ListenableFuture<Response> execute(Request request) {
return stickyRouter.execute(request, delegate);
}

@Override
public String toString() {
return "StickyEndpointChannel{delegate=" + delegate + '}';
}
Comment on lines -258 to -261

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seeing this makes me wonder whether anybody may have been logging the channel, as a way to debug stuff, and whether the loss of this override may cause debugging pain (since you're now returning request -> router.execute(request, endpointWithQueueOverride);)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.dialogue.core;

import com.palantir.dialogue.Channel;

public interface StickyEndpointChannelsFactory {
Comment on lines +17 to +21

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Change the package and make the name non-plural to be consistent with EndpointChannelFactory.

Also this returns Channel, not EndpointChannel.

Suggested change
package com.palantir.dialogue.core;
import com.palantir.dialogue.Channel;
public interface StickyEndpointChannelsFactory {
package com.palantir.dialogue;
import com.palantir.dialogue.Channel;
public interface StickyChannelFactory {

Channel stickyChannel();
}
Loading