-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Describe the bug
Three related bugs in StorkConfigUtil and StorkRegistrarConfigRecorder cause service
configuration corruption when multiple services use service-registrar config:
-
Builder reuse leaks config between services —
SimpleServiceConfig.Builderis created
once outside the loop intoStorkServiceConfig()(StorkConfigUtil.java, line 37).
build()does not reset fields, so conditionally-set values (discovery, LB, registrar)
leak from one iteration to the next. -
registrationConfigsfilter ignoresenabledstatus — The filter at
StorkRegistrarConfigRecorder.javalines 56-57 only checksserviceRegistrar() != null,
so disabled registrars (service-registrar.enabled: false) inflateregistrationConfigs.size()
and cause the wrong branch insetupServiceRegistrarConfig(). Combined with Bug 1, this
can push the count from 1 to 2. -
buildServiceConfigurationWithRegistrar()drops discovery and LB config —
(StorkConfigUtil.java, lines 118-136). When thesize() == 1branch
(StorkRegistrarConfigRecorder.java, line 62) callsaddRegistrarTypeIfAbsent()->
buildServiceConfigurationWithRegistrar(), the returnedServiceConfigurationhardcodes
Optional.empty()for discovery andnullfor load balancer, silently replacing the
original viacomputeIfPresent(lines 64-66).
Expected behavior
- Each service gets an isolated
SimpleServiceConfig— config from one service never
affects another. - Disabled registrars (
enabled: false) are excluded fromregistrationConfigs, so they
don't affect branch selection insetupServiceRegistrarConfig(). - When
addRegistrarTypeIfAbsentbuilds a newServiceConfiguration, it preserves the
original service's discovery and load-balancer settings.
Actual behavior
- Config set conditionally on one service leaks into subsequent services because the builder
is reused across loop iterations. - Disabled registrars pass the filter and inflate the count, potentially selecting the
multi-registrar branch when only one registrar is actually enabled. - Discovery and load-balancer config is silently dropped when a single-registrar service
has its config rebuilt.
How to Reproduce?
Bug 1 — configure two services where only the first has a registrar:
quarkus:
stork:
backend-1:
service-registrar:
type: eureka
backend-2:
service-discovery:
type: eureka| Iteration | setServiceRegistrar called? |
setServiceDiscovery called? |
Result |
|---|---|---|---|
backend-1 |
Yes (eureka) | No (no discovery config) | Correct: registrar only |
backend-2 |
No (no registrar config) | Yes (eureka) | Wrong: gets backend-1's registrar leaked in |
backend-2 ends up with a registrar it never configured.
The builder reuse is at StorkConfigUtil.java line 37:
SimpleServiceConfig.Builder builder = new SimpleServiceConfig.Builder(); // <-- only one builder
for (String serviceName : servicesConfigs) {
builder.setServiceName(serviceName);
// ...fields set conditionally...
storkServicesConfigs.add(builder.build()); // previous fields leak through
}Bug 2 — configure two services where one has its registrar explicitly disabled:
quarkus:
stork:
backend-1:
service-registrar:
type: eureka
backend-2:
service-registrar:
type: eureka
enabled: falsebackend-2's registrar is disabled, so only backend-1 should be treated as having an
active registrar (registrationConfigs.size() should be 1). But the filter at lines 56-57
only checks for != null, so both pass and the count becomes 2, selecting the wrong branch:
List<ServiceConfig> registrationConfigs = serviceConfigs.stream()
.filter(serviceConfig -> serviceConfig.serviceRegistrar() != null).toList();failOnMissingRegistrarTypesForMultipleRegistrars does check enabled (lines 75-76), but
by then the branching decision has already been made.
Bug 3 — configure a single service with both registrar and discovery:
quarkus:
stork:
my-service:
service-registrar:
type: eureka
service-discovery:
type: eurekaAfter setupServiceRegistrarConfig() runs, the service's discovery and load-balancer config
are gone — replaced by hardcoded empty values in buildServiceConfigurationWithRegistrar().
Output of uname -a or ver
Linux linux-mint 6.8.0-106-generic #106-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 6 07:58:08 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
Output of java -version
openjdk version "25.0.2" 2026-01-20 LTS
Quarkus version or git rev
3.32.2
Build tool (ie. output of mvnw --version or gradlew --version)
Apache Maven 3.9.12
Additional information
Suggested fixes:
Bug 1 — move builder inside loop:
for (String serviceName : servicesConfigs) {
SimpleServiceConfig.Builder builder = new SimpleServiceConfig.Builder(); // fresh per service
builder.setServiceName(serviceName);
// ... rest unchanged ...
storkServicesConfigs.add(builder.build());
}Bug 2 — extract an enabled-aware filter:
List<ServiceConfig> registrationConfigs = serviceConfigs.stream()
.filter(this::hasEnabledRegistrar)
.toList();
private boolean hasEnabledRegistrar(ServiceConfig config) {
return Optional.ofNullable(config.serviceRegistrar())
.map(r -> r.parameters().getOrDefault("enabled", "true"))
.map(Boolean::parseBoolean)
.orElse(false);
}The duplicate enabled check in failOnMissingRegistrarTypesForMultipleRegistrars can
then be removed:
private static void failOnMissingRegistrarTypesForMultipleRegistrars(List<ServiceConfig> registrationConfigs) {
List<String> servicesWithMissingType = new ArrayList<>();
for (ServiceConfig registrationConfig : registrationConfigs) {
if (registrationConfig.serviceRegistrar().type().isBlank()) {
servicesWithMissingType.add(registrationConfig.serviceName());
LOGGER.info("Missing 'type' for service '" + registrationConfig.serviceName()
+ "'. This may lead to a runtime error.");
}
}
// ... rest unchanged ...
}Bug 3 — keep the existing 3-param buildServiceConfigurationWithRegistrar (used by
buildDefaultRegistrarConfiguration to create fresh configs), and add a 4-param overload
that preserves existing discovery/LB settings:
// New overload — used by addRegistrarTypeIfAbsent to preserve the original config
private static ServiceConfiguration buildServiceConfigurationWithRegistrar(
ServiceConfiguration original, String type, boolean enabled,
Map<String, String> parameters) {
return new ServiceConfiguration() {
@Override
public Optional<StorkServiceDiscoveryConfiguration> serviceDiscovery() {
return original.serviceDiscovery(); // preserve
}
@Override
public StorkLoadBalancerConfiguration loadBalancer() {
return original.loadBalancer(); // preserve
}
@Override
public Optional<StorkServiceRegistrarConfiguration> serviceRegistrar() {
return Optional.of(buildServiceRegistrarConfiguration(type, enabled, parameters));
}
};
}Then update addRegistrarTypeIfAbsent to pass the original serviceConfiguration through:
private ServiceConfiguration addRegistrarTypeIfAbsent(ServiceConfiguration serviceConfiguration,
String serviceRegistrarType, Map<String, String> parameters) {
// ...
return buildServiceConfigurationWithRegistrar(
serviceConfiguration, serviceRegistrarType, true, parameters);
}The existing 3-param buildServiceConfigurationWithRegistrar(type, enabled, parameters) is
left unchanged for buildDefaultRegistrarConfiguration, which creates new configs from scratch.