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

loadbalancer: HostSelector can be rebuilt each time the DefaultLoadBalancer gets a host set update #2774

Merged

Conversation

bryce-anderson
Copy link
Contributor

@bryce-anderson bryce-anderson commented Dec 7, 2023

Motivation:

For algorithms like weighted round robin and random we need to
build some data structures for each unique host set.

Modifications:

  • Add a .rebuild(List<Host>) method to the HostSelector
    interface. This lets a host potentially rebuild data structures it
    needs to properly balance but only when there is a host set update.
  • Add a .isHealthy() method to HostSelector. This cleaned up
    some questions about which host list is used and may also be
    helpful for future composition of selectors.
  • Cleanup some thread safety considerations in DefaultLoadBalancer.

Motivation:

For algorithms like weighted round robin and weighted
random we need to build some data structures for each
unique host set.

Modifications:

Add a `.rebuild(List<Host>)` method to the `HostSelector`
interface. This lets a host potentially rebuild data
structures it needs to properly balance but only when
there is a host set update.
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

abstract class LoadBalancerTestScaffold {
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 all pulled from LoadBalancerTest.

@@ -127,33 +111,12 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

abstract class LoadBalancerTest {
abstract class LoadBalancerTest extends LoadBalancerTestScaffold {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All the deletions here were simply moved to LoadBalancerTestScaffold.


SequentialExecutor(final ExceptionHandler exceptionHandler) {
this.exceptionHandler = requireNonNull(exceptionHandler, "exceptionHandler");
}

public boolean isCurrentThreadDraining() {
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 only really necessary so we can make DefaultLoadBalancer.usedHosts non-volatile and still have a thread safe DefaultLoadBalancer.usedAddresses() method, but it feels 'nice'. Feedback welcome on it's utility.
Also see the comment in usedAddresses().

Comment on lines 520 to 532
// If we're already in the executor we can't submit a task and wait for it without deadlock but
// the access is thread safe anyway so just go for it.
if (sequentialExecutor.isCurrentThreadDraining()) {
return sequentialUsedAddresses();
}
CompletableFuture<List<Entry<ResolvedAddress, List<C>>>> future = new CompletableFuture<>();
sequentialExecutor.execute(() -> future.complete(sequentialUsedAddresses()));
try {
// This method is just for testing, so it's fine to do some awaiting.
return future.get(5, TimeUnit.SECONDS);
} catch (Exception ex) {
throw new AssertionError("Failed to get results", ex);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we wanted to, this pattern could be lifted into a method of the form T SequentialExecutor.submit(Callable<T>).

@bryce-anderson bryce-anderson marked this pull request as ready for review December 8, 2023 00:06
@bryce-anderson bryce-anderson changed the title loadbalancer: HostSelector can be rebuilt loadbalancer: HostSelector can be rebuilt each time the DefaultLoadBalancer gets a host set update Dec 8, 2023
Copy link
Member

@idelpivnitskiy idelpivnitskiy left a comment

Choose a reason for hiding this comment

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

LGTM, please get a 2nd eye on SequentialExecutor and removal of volatile from DefaultLoadBalancer

@@ -502,6 +509,24 @@ public Completable closeAsyncGracefully() {

@Override
public List<Entry<ResolvedAddress, List<C>>> usedAddresses() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be missing something here, but isn't that a check-then-act issue? What guarantees that the condition still holds after the IF.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you clarify what your concern is? If it's whether the result from usedAddresses() is still valid, then yes: it's always going to be racy. Since it's only used for tests to inspect the internal state the usage is rather constrained it should be no worse than it always was. If you means the if block on line 514 then we know what the current sequence of calls are going to be for this thread and reason about it for the lifetime of this method call at least.

* pattern to use is to make the internal state effectively immutable and rebuilds (see below) generate new
* immutable instances as necessary. The interface is expected to adhere to the following rules:
*
* <li>The {@link HostSelector#selectConnection(Predicate, ContextMap, boolean)} method will be used
Copy link
Contributor

Choose a reason for hiding this comment

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

So to clarify: the old host selector could still be called even if a new rebuilt host selector has been returned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is not strictly specified. It's possible to envision a use case where the old-selector gets 'locked down' when a new selector is built. However, the common case (and perhaps the only real case) should be that sending a request to an old selector will be treated as if it had simply raced ahead of the rebuild.

@bryce-anderson bryce-anderson merged commit bad9558 into apple:main Dec 11, 2023
15 checks passed
@bryce-anderson bryce-anderson deleted the bl_anderson/rebuild-selector branch December 11, 2023 19:21
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

4 participants