apache / apache/dubbo

[Bug] Dual registration: one registry's failure aborts the sibling arm under register-mode=all

Open
#16,247 0 comments 0 reactions 0 assignees View on GitHub
type/enhancement
Dominant language
Java
Stars
41.6k
Forks
26.4k
Avg merge
15h 13m
Merged PRs (30d)
4

Description

### Pre-check

- [x] I am sure that all the content I provide is in English.

### Search before asking

- [x] I had searched in the [issues](https://github.com/apache/dubbo/issues?q=is%3Aissue) and found no similar issues.

### Apache Dubbo Component

Java SDK (apache/dubbo)

### Dubbo Version

Dubbo Java 3.3.7-SNAPSHOT

### Steps to reproduce this issue

Pre-requisite: a provider configured with `register-mode=all` so Dubbo fans out the same provider URL to two registries — an interface-level one
(`registry://`, e.g. ZooKeeper) and an instance-level one (`service-discovery-registry://`).

1. Configure the provider so both registries are in use, for example:

```properties
dubbo.application.register-mode=all
dubbo.registries.zk.address=zookeeper://127.0.0.1:2181
dubbo.registries.sd.address=zookeeper://127.0.0.1:2181?registry-type=service
```

2. Make the **first** registry fail at registration time (easiest repro:
start the provider with ZooKeeper unreachable, or let the interface-level
registry throw `NoNode` during the write). The failing registry can be
either arm — whichever is processed first.

3. Export a service:

```java
ServiceConfig sc = new ServiceConfig<>();
sc.setInterface(DemoService.class);
sc.setRef(new DemoServiceImpl());
sc.export();
```

4. Observe that `export()` throws the underlying `RuntimeException` from the
failing arm and the **second** registry is never registered to, even
though the two registrations are semantically independent and the user
explicitly opted into dual mode with `register-mode=all`.

Root cause is in `RegistryProtocol#export`:

```java
// dubbo-registry-api/.../RegistryProtocol.java
boolean register = providerUrl.getParameter(REGISTER_KEY, true)
&& registryUrl.getParameter(REGISTER_KEY, true);
if (register) {
register(registry, registeredProviderUrl); // <-- not guarded per-arm
}
```

The single call fans out to both arms and any `RuntimeException` from one
arm aborts the whole export.

A minimal unit-test reproducer (no external registry needed) using a mocked
`Registry`:

```java
URL registryUrl = new ServiceConfigURL("zookeeper", "127.0.0.1", 2181,
Map.of("check", "false"));
URL providerUrl = new ServiceConfigURL("dubbo", "127.0.0.1", 20880,
"com.example.DemoService", new HashMap<>());
Registry registry = Mockito.mock(Registry.class);
Mockito.doThrow(new IllegalStateException("NoNode for /dubbo/..."))
.when(registry).register(providerUrl);

// Through the current export() -> register() path, this RuntimeException
// propagates and any sibling registration never runs; there is also no
// per-arm outcome emitted to FrameworkStatusReportService.
```

### What you expected to happen

Under `register-mode=all`, each arm of the dual registration should be
handled independently, and outcomes should be observable per arm:

1. **Failure isolation.** A failure on one arm (interface-level OR
instance-level) must not abort the other arm. Propagation should only
happen when the registry URL explicitly asks for `check=true` — which
is the convention Dubbo already uses elsewhere for `CHECK_KEY`.
2. **Per-arm outcome reporting.** `FrameworkStatusReportService` should
emit an outcome that identifies which arm was attempted, against which
registry address, for which service, and whether it succeeded or
failed (and why). Today only a coarse `{application, status}` payload
is emitted, so operators cannot tell which side of the dual
registration failed.
3. **Accurate log tags.** The success log currently hardcodes
`[INSTANCE_REGISTER]` in `ExporterChangeableWrapper#register`
regardless of whether the registry is actually an instance-level one,
which makes grep-based troubleshooting misleading for interface-level
arms.

What currently happens instead:

- **(1)** The `RuntimeException` from the first arm aborts `export()` and
the second arm is never tried. Users who opted into dual mode lose the
healthy arm because the other arm was transiently unhealthy.
- **(2)** Observers subscribed to the `registration` report key only see
`{application, status=interface|instance}`. They cannot attribute a
partial failure to a specific arm/registry/service, and they never see
an error message.
- **(3)** Relevant log line:

```
[INSTANCE_REGISTER] Registered dubbo service ... to registry zookeeper://...
```

…is printed even when the registry is `zookeeper://` (interface-level).

Concretely, the expected payload shape for the outcome report is:

```json
{
"application": "APP",
"mode": "INTERFACE_REGISTER", // or INSTANCE_REGISTER
"registry": "127.0.0.1:2181",
"service": "GroupA/DemoService:1.0.0",
"status": "SUCCESS", // or FAILED
"error": "NoNode for /dubbo/..." // present only on FAILED
}
```

emitted under the same `REGISTRATION_STATUS` key so existing
`FrameworkStatusReporter` extensions keep working (new fields are
additive).

### Anything else

- Occurs deterministically: every time one arm of a dual registration
throws a `RuntimeException` synchronously. Not a flake.
- The existing logger code `CONFIG_REGISTER_INSTANCE_ERROR` (5-11) is a
natural fit for the failure path — no new code is needed.
- Backward compatibility:
- No public API removed.
- `reportRegistrationStatus(...)` / `createRegistrationReport(...)`
untouched; reporters that only read `application` / `status` keep
working.
- Users who relied on "one failure aborts everything" can keep that by
setting `check=true` on the registry URL, which is the documented
knob for "registration must succeed or fail fast".
- Consumer-side `registry.register(...)` (for consumer URLs) is **not**
part of this report; the scope here is provider-side dual registration
only.

### Do you have a (mini) reproduction demo?

- [x] Yes, I have a minimal reproduction demo to help resolve this issue more effectively!

### Are you willing to submit a pull request to fix on your own?

- [x] Yes I am willing to submit a pull request on my own!

### Code of Conduct

- [x] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct)

Contributor guide

Open the contributing guide

Research direction

Start in dubbo-registry-api/.../RegistryProtocol.java at RegistryProtocol#export and trace the register() path for both registry arms. Then inspect ExporterChangeableWrapper#register and the FrameworkStatusReportService reporting methods, using the mocked Registry reproduction as the first test case. Done means one arm can fail without preventing the sibling arm, each outcome identifies its arm and failure, and the success log tag matches the registry type.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend-api-design, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.