diff --git a/core/src/main/java/io/grpc/internal/ServiceConfigState.java b/core/src/main/java/io/grpc/internal/ServiceConfigState.java
new file mode 100644
index 0000000000..da4c4ce101
--- /dev/null
+++ b/core/src/main/java/io/grpc/internal/ServiceConfigState.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2019 The gRPC Authors
+ *
+ * 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 io.grpc.internal;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import io.grpc.NameResolver.ConfigOrError;
+import javax.annotation.Nullable;
+
+/**
+ * {@link ServiceConfigState} holds the state of the current service config. It must be mutated
+ * and read from {@link ManagedChannelImpl} constructor or the provided syncContext.
+ */
+final class ServiceConfigState {
+ @Nullable private final ConfigOrError defaultServiceConfig;
+ private final boolean lookUpServiceConfig;
+
+ // mutable state
+ @Nullable private ConfigOrError currentServiceConfigOrError;
+ // Has there been at least one update?
+ private boolean updated;
+
+ /**
+ * @param defaultServiceConfig The initial service config, or {@code null} if absent.
+ * @param lookUpServiceConfig {@code true} if service config updates might occur.
+ * @param syncCtx The synchronization context that this is accessed from.
+ */
+ ServiceConfigState(
+ @Nullable ManagedChannelServiceConfig defaultServiceConfig,
+ boolean lookUpServiceConfig) {
+ if (defaultServiceConfig == null) {
+ this.defaultServiceConfig = null;
+ } else {
+ this.defaultServiceConfig = ConfigOrError.fromConfig(defaultServiceConfig);
+ }
+ this.lookUpServiceConfig = lookUpServiceConfig;
+ if (!lookUpServiceConfig) {
+ this.currentServiceConfigOrError = this.defaultServiceConfig;
+ }
+ }
+
+ /**
+ * Returns {@code true} if it RPCs should wait on a service config resolution. This can return
+ * {@code false} if:
+ *
+ *
+ * - There is a valid service config from the name resolver
+ *
- There is a valid default service config and a service config error from the name
+ * resolver
+ *
- No service config from the name resolver, and no intent to lookup a service config.
+ *
+ *
+ * In the final case, the default service config may be present or absent, and will be the
+ * current service config.
+ */
+ boolean shouldWaitOnServiceConfig() {
+ return !(updated || !expectUpdates());
+ }
+
+ /**
+ * Gets the current service config or error.
+ *
+ * @throws IllegalStateException if the service config has not yet been updated.
+ */
+ @Nullable
+ ConfigOrError getCurrent() {
+ checkState(!shouldWaitOnServiceConfig(), "still waiting on service config");
+ return currentServiceConfigOrError;
+ }
+
+ void update(@Nullable ConfigOrError coe) {
+ checkState(expectUpdates(), "unexpected service config update");
+ boolean firstUpdate = !updated;
+ updated = true;
+ if (firstUpdate) {
+ if (coe == null) {
+ currentServiceConfigOrError = defaultServiceConfig;
+ } else if (coe.getError() != null) {
+ if (defaultServiceConfig != null) {
+ currentServiceConfigOrError = defaultServiceConfig;
+ } else {
+ currentServiceConfigOrError = coe;
+ }
+ } else {
+ assert coe.getConfig() != null;
+ currentServiceConfigOrError = coe;
+ }
+ } else {
+ if (coe == null) {
+ if (defaultServiceConfig != null) {
+ currentServiceConfigOrError = defaultServiceConfig;
+ } else {
+ currentServiceConfigOrError = null;
+ }
+ } else if (coe.getError() != null) {
+ if (currentServiceConfigOrError != null && currentServiceConfigOrError.getError() != null) {
+ currentServiceConfigOrError = coe;
+ } else {
+ // discard
+ }
+ } else {
+ assert coe.getConfig() != null;
+ currentServiceConfigOrError = coe;
+ }
+ }
+ }
+
+ boolean expectUpdates() {
+ return lookUpServiceConfig;
+ }
+}
diff --git a/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java b/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java
new file mode 100644
index 0000000000..996401218f
--- /dev/null
+++ b/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2019 The gRPC Authors
+ *
+ * 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 io.grpc.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import io.grpc.NameResolver.ConfigOrError;
+import io.grpc.Status;
+import io.grpc.internal.ManagedChannelServiceConfig.MethodInfo;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link ServiceConfigState}.
+ */
+@RunWith(JUnit4.class)
+public class ServiceConfigStateTest {
+
+ private final ManagedChannelServiceConfig serviceConfig1 = new ManagedChannelServiceConfig(
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ null,
+ null);
+ private final ManagedChannelServiceConfig serviceConfig2 = new ManagedChannelServiceConfig(
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ null,
+ null);
+ private final ConfigOrError config1 = ConfigOrError.fromConfig(serviceConfig1);
+ private final ConfigOrError config2 = ConfigOrError.fromConfig(serviceConfig2);
+ private final Status serviceConfigError1 = Status.UNKNOWN.withDescription("bang");
+ private final Status serviceConfigError2 = Status.INTERNAL.withDescription("boom");
+ private final ConfigOrError error1 = ConfigOrError.fromError(serviceConfigError1);
+ private final ConfigOrError error2 = ConfigOrError.fromError(serviceConfigError2);
+ private final ManagedChannelServiceConfig noServiceConfig = null;
+ private final ConfigOrError noConfig = null;
+
+ @Test
+ public void noLookup_default() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, false);
+
+ assertFalse(scs.expectUpdates());
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void noLookup_default_allUpdatesFail() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, false);
+
+ try {
+ scs.update(error1);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ try {
+ scs.update(error1);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ try {
+ scs.update(noConfig);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void noLookup_noDefault() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, false);
+
+ assertFalse(scs.expectUpdates());
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void noLookup_noDefault_allUpdatesFail() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, false);
+
+ try {
+ scs.update(error1);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+
+ try {
+ scs.update(config2);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+
+ try {
+ scs.update(noConfig);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("unexpected service config update");
+ }
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ assertTrue(scs.expectUpdates());
+ assertTrue(scs.shouldWaitOnServiceConfig());
+ try {
+ scs.getCurrent();
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("still waiting on service config");
+ }
+ }
+
+ @Test
+ public void lookup_noDefault_onError_onError() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(error1, scs.getCurrent());
+
+ scs.update(error2);
+ assertSame(error2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onError_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(error1, scs.getCurrent());
+
+ scs.update(noConfig);
+ assertNull(scs.getCurrent());
+
+ scs.update(error2);
+ assertNull(scs.getCurrent()); //ignores future errors
+ }
+
+ @Test
+ public void lookup_noDefault_onError_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(error1, scs.getCurrent());
+
+ scs.update(config1);
+ assertSame(config1, scs.getCurrent());
+
+ scs.update(error2);
+ assertSame(config1, scs.getCurrent()); //ignores future errors
+ }
+
+ @Test
+ public void lookup_noDefault_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onAbsent_onError() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+
+ scs.update(error1);
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onAbsent_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+
+ scs.update(noConfig);
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onAbsent_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertNull(scs.getCurrent());
+
+ scs.update(config1);
+ assertSame(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(config1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onPresent_onError() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(config1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config1, scs.getCurrent());
+
+ scs.update(error1);
+ assertSame(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onPresent_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(config1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config1, scs.getCurrent());
+
+ scs.update(noConfig);
+ assertNull(scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_noDefault_onPresent_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(noServiceConfig, true);
+
+ scs.update(config1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config1, scs.getCurrent());
+
+ scs.update(config2);
+ assertSame(config2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ assertTrue(scs.expectUpdates());
+ assertTrue(scs.shouldWaitOnServiceConfig());
+ try {
+ scs.getCurrent();
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessageThat().contains("still waiting on service config");
+ }
+ }
+
+ @Test
+ public void lookup_default_onError() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onError_onError() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(error2);
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onError_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(noConfig);
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onError_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(error1);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(config2);
+ assertSame(config2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onAbsent_onError() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(error1);
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onAbsent_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(noConfig);
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onAbsent_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(noConfig);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSameConfig(config1, scs.getCurrent());
+
+ scs.update(config2);
+ assertSame(config2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(config2);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onPresent_onError() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(config2);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config2, scs.getCurrent());
+
+ scs.update(error1);
+ assertSame(config2, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onPresent_onAbsent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+
+ scs.update(config2);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config2, scs.getCurrent());
+
+ scs.update(noConfig);
+ assertSameConfig(config1, scs.getCurrent());
+ }
+
+ @Test
+ public void lookup_default_onPresent_onPresent() {
+ ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
+ ManagedChannelServiceConfig serviceConfig3 = new ManagedChannelServiceConfig(
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ null,
+ null);
+ ConfigOrError config3 = ConfigOrError.fromConfig(serviceConfig3);
+
+ scs.update(config2);
+ assertFalse(scs.shouldWaitOnServiceConfig());
+ assertSame(config2, scs.getCurrent());
+
+ scs.update(config3);
+ assertSame(config3, scs.getCurrent());
+ }
+
+ private static void assertSameConfig(ConfigOrError expected, ConfigOrError actual) {
+ if (actual == null || expected == null) {
+ assertSame(expected, actual);
+ }
+ assertWithMessage(actual.toString()).that(actual.getConfig())
+ .isSameInstanceAs(expected.getConfig());
+ }
+}