AlexStocks opened a new issue, #3248:
URL: https://github.com/apache/dubbo-go/issues/3248

   ## Background
   
   After auditing the dubbo-go codebase for memory leaks (#3246) and data races 
(#3247), the root causes point to several structural issues that have 
accumulated over the project's 10-year history. This issue proposes a 
prioritized improvement roadmap.
   
   ---
   
   ## 1. Redesign `common.URL` — the core data structure needs a major overhaul
   
   `common.URL` is the single most important type in the framework. Every 
invoker, exporter, registry, and configuration path revolves around it. But it 
has grown organically into a god object with serious problems:
   
   **Current issues:**
   - `params` (url.Values) exposed via `GetParams()` without lock or copy — 
callers mutate internals
   - `attributes` (map[string]any) is write-only — no `DeleteAttribute`, no 
`ClearAttributes`
   - `AddParam` vs `SetParam` semantic confusion (`Add` appends, not 
overwrites) causing silent accumulation
   - `MergeURL` reads `anotherUrl.attributes` without `attributesLock` — data 
race
   - `GetCacheInvokerMapKey` allocates a full temporary URL on every call — 
unnecessary GC pressure
   - `SubURL` pointer creates implicit reference chains that prevent GC
   - Multiple key generation methods (`Key()`, `GetCacheInvokerMapKey()`, 
`ServiceKey()`) with different formats, causing key mismatch bugs (e.g., 
invoker blacklist)
   
   **Proposed redesign:**
   1. Make `params` immutable after construction — provide `WithParam(k, v)` 
that returns a new URL (or at minimum, return copies from getters)
   2. Add `DeleteAttribute(key)` and `ClearAttributes()` methods
   3. Deprecate `AddParam` — it's almost always misused when `SetParam` is 
intended
   4. Unify key generation into a single canonical `Key()` method
   5. Consider splitting URL into `URLSpec` (immutable identity) + `URLState` 
(mutable runtime state)
   6. Add `Clone()` that does proper deep copy of attributes
   
   ---
   
   ## 2. Make `common/extension/` concurrency-safe — introduce `Registry[T]`
   
   The extension package has 20+ global maps, all following the same unsafe 
pattern (no locks, some returning internal map references). This is the single 
largest source of potential crashes.
   
   **Proposed solution:** A generic thread-safe registry:
   
   ```go
   package extension
   
   type Registry[T any] struct {
       mu    sync.RWMutex
       items map[string]T
   }
   
   func NewRegistry[T any]() *Registry[T] { ... }
   func (r *Registry[T]) Register(name string, v T) { ... }
   func (r *Registry[T]) Get(name string) (T, bool) { ... }
   func (r *Registry[T]) MustGet(name string) T { ... }  // panics if missing
   func (r *Registry[T]) Unregister(name string) { ... }
   func (r *Registry[T]) Snapshot() map[string]T { ... }  // returns copy
   func (r *Registry[T]) Names() []string { ... }         // returns copy
   ```
   
   This single abstraction would fix all 22 files in `common/extension/` and 
eliminate an entire class of bugs. It also standardizes the API (current code 
mixes `Set*`/`Get*`/`Unregister*`/`GetAll*` inconsistently).
   
   ---
   
   ## 3. Implement proper lifecycle management for invokers/exporters
   
   Current state:
   - `registryProtocol.Destroy()` doesn't clean `overrideListeners` or 
`serviceConfigurationListeners`
   - `RegistryDirectory.Destroy()` doesn't clean `cacheInvokersMap`
   - Invoker blacklist entries become permanently orphaned due to key mismatch
   - `exporterChangeableWrapper` URLs are set *after* being stored in the map, 
creating a race window
   - No `Closeable`/`Lifecycle` interface enforced — each component invents its 
own cleanup pattern
   
   **Proposed improvements:**
   1. Define a `Lifecycle` interface: `Start()`, `Stop()`, `IsRunning() bool`
   2. Enforce cleanup contracts: every `Store` must have a corresponding 
`Delete` path
   3. Add integration tests that verify zero goroutine/memory leaks after full 
startup-shutdown cycle
   4. Use `context.Context` consistently for cancellation (replace `time.Sleep` 
polling patterns in nacos, etc.)
   
   ---
   
   ## 4. Enforce `go test -race` in CI
   
   Currently, many data races go undetected because `-race` is not mandatory in 
CI. Given the scale of race conditions found (#3247), this is the single most 
impactful process change.
   
   **Action items:**
   1. Add `-race` flag to CI test runs (at minimum for core packages)
   2. Fix all existing race detector failures
   3. Block merges on race detector failures
   
   ---
   
   ## 5. API hygiene — stop exposing internal state
   
   Several public APIs return internal references that callers can mutate, 
breaking encapsulation:
   
   | API | Returns | Risk |
   |-----|---------|------|
   | `URL.GetParams()` | Internal `url.Values` map | Caller mutation + race |
   | `GetDefaultConfigReader()` | Internal `defaults` map | Concurrent 
iteration crash |
   | `GetRouterFactories()` | Internal `routers` map | Concurrent iteration 
crash |
   | `GetProviderServiceMap()` | Internal `proServices` map | Concurrent 
iteration crash |
   | `GetConsumerServiceMap()` | Internal `conServices` map | Concurrent 
iteration crash |
   | `GetAllCustomShutdownCallbacks()` | Internal `*list.List` | Concurrent 
mutation |
   | `GetCustomizers()` | Internal slice reference | Concurrent append+sort |
   
   **Rule**: Public getters must return copies, never internal references. This 
should be a review checklist item.
   
   ---
   
   ## 6. Reduce `config` package coupling
   
   The ongoing refactor (PRs #3231, #3232) to remove `config` package 
dependencies from protocol implementations is the right direction. But the 
approach needs more care:
   
   - **Don't silently change error semantics** (e.g., `LoadRegistries` changing 
from panic to silent skip)
   - **Don't break public APIs without deprecation** (e.g., `WithMethod` 
signature change)
   - **Validate configuration at init time** (don't remove init-time validation 
without replacement)
   - **Use URL attributes consistently** — document which attributes are 
expected and where they're set
   
   ---
   
   ## 7. Standardize error handling — eliminate `panic` in library code
   
   The extension package uses `panic` for missing registrations:
   
   ```go
   func GetProtocol(name string) base.Protocol {
       if protocols[name] == nil {
           panic("protocol for [" + name + "] is not existing, ...")
       }
   }
   ```
   
   This is inappropriate for a library/framework. A missing extension should 
return an error, not crash the entire process. The caller can decide whether to 
panic.
   
   **Rule**: Reserve `panic` for truly unrecoverable programmer errors. 
Extension lookups should return `(T, error)`.
   
   ---
   
   ## Priority
   
   | Priority | Item | Impact | Effort |
   |----------|------|--------|--------|
   | P0 | Fix `dubbo.go:301` wrong lock | Crash prevention | 1 line |
   | P0 | Enable `-race` in CI | Prevent future regressions | Config change |
   | P0 | `extension.Registry[T]` | Fix 20+ race-prone files | Medium (1 week) |
   | P1 | URL `DeleteAttribute` + cleanup | Memory leak prevention | Small |
   | P1 | Fix `RouterChain.Route` lockless read | Crash prevention | Small |
   | P1 | Return copies from public getters | Crash prevention | Small |
   | P2 | URL redesign | Long-term maintainability | Large |
   | P2 | Lifecycle interface | Leak prevention | Medium |
   | P2 | Eliminate panics | API quality | Medium |
   
   Happy to help with implementation of any of these items. Related issues: 
#3246, #3247.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to