Skip to content

quarkus-smallrye-stork: Service-registrar config setup silently corrupts service configuration #53092

@bumann-p

Description

@bumann-p

Describe the bug

Three related bugs in StorkConfigUtil and StorkRegistrarConfigRecorder cause service
configuration corruption when multiple services use service-registrar config:

  1. Builder reuse leaks config between servicesSimpleServiceConfig.Builder is created
    once outside the loop in toStorkServiceConfig() (StorkConfigUtil.java, line 37).
    build() does not reset fields, so conditionally-set values (discovery, LB, registrar)
    leak from one iteration to the next.

  2. registrationConfigs filter ignores enabled status — The filter at
    StorkRegistrarConfigRecorder.java lines 56-57 only checks serviceRegistrar() != null,
    so disabled registrars (service-registrar.enabled: false) inflate registrationConfigs.size()
    and cause the wrong branch in setupServiceRegistrarConfig(). Combined with Bug 1, this
    can push the count from 1 to 2.

  3. buildServiceConfigurationWithRegistrar() drops discovery and LB config
    (StorkConfigUtil.java, lines 118-136). When the size() == 1 branch
    (StorkRegistrarConfigRecorder.java, line 62) calls addRegistrarTypeIfAbsent() ->
    buildServiceConfigurationWithRegistrar(), the returned ServiceConfiguration hardcodes
    Optional.empty() for discovery and null for load balancer, silently replacing the
    original via computeIfPresent (lines 64-66).

Expected behavior

  • Each service gets an isolated SimpleServiceConfig — config from one service never
    affects another.
  • Disabled registrars (enabled: false) are excluded from registrationConfigs, so they
    don't affect branch selection in setupServiceRegistrarConfig().
  • When addRegistrarTypeIfAbsent builds a new ServiceConfiguration, 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: false

backend-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: eureka

After 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions