refactor(config): merge config files with BeanDefaults fallback#6752
refactor(config): merge config files with BeanDefaults fallback#6752317787106 wants to merge 40 commits intotronprotocol:developfrom
Conversation
…r twice; add several methods of HttpServletRequestWrapper
Resolve conflict by keeping deletion of common/src/main/resources/reference.conf, which is intentional — BeanDefaults replaces its role in this branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rible; fix the bug of allowShieldedTransactionApi
| String key = pd.getName(); | ||
| Object value = getter.invoke(bean); | ||
| map.put(key, toValue(value)); | ||
| } catch (Exception ignored) { |
There was a problem hiding this comment.
[SHOULD] Silent swallow of getter exceptions hides root cause
The catch (Exception ignored) here drops any getter failure (typically InvocationTargetException from a buggy bean getter) without any log record. When this happens, the affected property is silently absent from the defaults map and ConfigBeanFactory later throws Missing path xxx pointing at user config — the real cause (a broken bean getter) becomes invisible to operators and to anyone debugging a startup failure.
Keep the best-effort behavior, but add a debug-level log so the root cause is recoverable when needed (e.g. via -Dlogger.org.tron.core.config.BeanDefaults=DEBUG).
Suggestion: instantiate a private static Logger and call logger.debug("Skipping property {}.{} due to introspection failure", bean.getClass().getName(), pd.getName(), e) inside the catch block.
| // BeanDefaults does not emit this legacy key, so hasPath here reliably means the user | ||
| // set it in their config. When present, it overrides the modern key. | ||
| if (section.hasPath("fullNodeAllowShieldedTransaction")) { | ||
| if (!userSection.hasPath("allowShieldedTransactionApi") && |
There was a problem hiding this comment.
[SHOULD] Priority semantics flip for allowShieldedTransactionApi is a behavior change, not a bug fix
The PR description frames this as fixing a dead-code bug, but in develop today the legacy branch is not dead — it actually overrides the modern key whenever a user sets fullNodeAllowShieldedTransaction. The old test testShieldedApiLegacyKeyTakesPriorityOverModern explicitly asserts legacy-wins. This PR flips that to modern-wins, and replaces the test accordingly.
That is a deliberate semantic change — not a refactor — and users who relied on legacy-wins (e.g. ops scripts that only set the legacy key while a stale allowShieldedTransactionApi lingers in their config) will see different behavior after upgrade.
Suggestion: add a "Behavior changes" section to the PR description making the priority flip explicit, and define a deprecation timeline for the legacy key (e.g. emit warn for two releases, remove in the third).
There was a problem hiding this comment.
@bladehan1 It doesn't belongs to behavior change, it remains same as v.4.8.1 that introduced in PR 6371. It fixs the bug that exists in PR 6615.
|
|
||
| private static Object toValue(Object value) { | ||
| if (value == null) { | ||
| return ""; |
There was a problem hiding this comment.
[NIT] toValue(null) → "" silently masks type mismatches
toValue(null) returns an empty string regardless of the declared field type. Today every nested bean field is eagerly initialized (private DiscoveryConfig discovery = new DiscoveryConfig();), so this never triggers — but a future change to lazy-init style (private DiscoveryConfig discovery;) would silently emit discovery = "" into the defaults map, and ConfigBeanFactory would then fail to bind a String value into a nested bean type with an obscure error.
Suggestion: either restrict the null → "" coercion to declared String types only, or throw IllegalStateException when a nested-bean / numeric / List getter returns null, so the broken bean is identified up-front rather than failing later in ConfigBeanFactory.
| import com.typesafe.config.Config; | ||
| import com.typesafe.config.ConfigBeanFactory; | ||
| import com.typesafe.config.ConfigFactory; | ||
| import org.tron.core.config.BeanDefaults; |
There was a problem hiding this comment.
[NIT] Import order: project imports placed between third-party imports
import org.tron.core.config.BeanDefaults; is inserted between com.typesafe.config.* and lombok.* imports, which breaks the project's Google Java Style import grouping (java/javax → com → org → lombok, with org.tron colocated with other org.*). Several other refactored files have the same issue (StorageConfig, etc.) — checkstyle CI will flag them.
Suggestion: run ./gradlew checkstyleMain locally and let it auto-format imports; the same fix should be applied to the other refactored config files.
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** |
There was a problem hiding this comment.
[NIT] Class Javadoc lacks usage-scope constraint
BeanDefaults exposes three public static entry points (toConfig / stripNullLeaves / remapKey). The Javadoc explains the design intent (ConfigBeanFactory fallback) but does not say "use here only." Because this class lives in the common/ leaf module, business modules (actuator, chainbase, etc.) could plausibly start using toConfig as a generic bean-to-Config serializer, which would couple a leaf utility to runtime concerns it was not designed for.
Suggestion: extend the class-level Javadoc with a one-line constraint such as: "Intended exclusively for ConfigBeanFactory fallback inside org.tron.core.config.args.*. For general bean serialization, use Jackson/Gson directly."
| .withFallback(ConfigFactory.defaultReference()); | ||
| config = ConfigFactory.parseFile(confFile); | ||
| } else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName) | ||
| != null) { |
There was a problem hiding this comment.
[NIT] Document that reference.conf is no longer expected on the load() path
ConfigFactory.load(fileName) historically picked up the in-jar reference.conf automatically. After this PR, reference.conf is deleted, but a stale copy left over in a fat-jar by a dirty build cache would still be loaded by this branch — silently masking the new bean-based defaults.
Suggestion: add a comment here noting that reference.conf was removed in this PR and any residual copy in the resource path is a build artifact to be cleaned; consider an optional packaging-time check that asserts no reference.conf ends up in the published jar.
| } | ||
|
|
||
| @Test | ||
| public void stripNullLeaves_removesNullPaths() { |
There was a problem hiding this comment.
[NIT] stripNullLeaves coverage is limited to String-typed null leaves
The current tests cover null leaves at scalar String fields and inside a nested object. They do not cover three other shapes that real users will produce by mistake:
- Numeric field set to null (e.g.
vm.maxEnergyLimitForConstant = null) - Whole nested object set to null (e.g.
storage.dbSettings = null) - List field set to null (e.g.
node.fastForward = null)
For each, the expected behavior is: stripNullLeaves removes the null and BeanDefaults provides the field default, so the node still starts.
Suggestion: add three tests, one per shape above. If the current implementation does not handle one of them correctly, tighten either stripNullObject or document the limit in the Javadoc of stripNullLeaves.
| * <p>List fields (active, passive, fastForward, disabledApi) auto-bind via | ||
| * ConfigBeanFactory's List<String> support. | ||
| */ | ||
| public static NodeConfig fromConfig(Config config) { |
There was a problem hiding this comment.
[QUESTION] Add automated guard for fields whose names start with two consecutive uppercase letters
The pBFTEnable / pBFTPort / pBFTExpireNum cases are handled by manual BeanDefaults.remapKey calls, but the rule "any field whose JavaBean property name starts with two consecutive uppercase letters needs a remap" lives only as commentary. A future contributor adding (for example) xPBFTSomething will likely forget to remap, and ConfigBeanFactory will silently fall back to the bean default while ignoring the user's value.
Question: would you consider adding a unit test that uses reflection to enumerate all *Config fields, detects names matching \b[A-Z]{2,} after the first letter, and asserts each one is either (a) absent from config.conf, or (b) covered by a remapKey call? It's a small one-time investment that turns a silent failure mode into a CI failure.
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** |
There was a problem hiding this comment.
[QUESTION] Provide a default-value parity check vs the deleted reference.conf
In the old model reference.conf was a runtime requirement — without it ConfigBeanFactory would throw Missing — so its values were the actual runtime defaults, and the bean field initializers (private int maxConnections = 30) were effectively dead code (always overwritten via the fallback chain). After this PR the bean field initializers become live for the first time. Any historical drift between a field initializer and the value that used to live in reference.conf is now a silent user-observable behavior change for anyone running with their own config.conf.
Manual spot-checking of the operationally critical keys (seed.node.ip.list, event.subscribe.topics, node.fastForward, genesis.block.*) confirms the shipped framework/src/main/resources/config.conf carries equivalent values, so users on the shipped template are unaffected. But every scalar key needs the same per-key check, and there is no automated guard against future drift.
Question: could the team commit the pre-deletion reference.conf as common/src/test/resources/reference.conf.legacy-fixture and add a parity test (BeanDefaults.toConfig(...) chained over all 9 beans, compared key-by-key against the fixture, with intentional differences explicitly whitelisted)? It would lock the contract permanently and catch future drift in CI.
If a parity test is too heavy for this PR, the minimum ask is to include the per-scalar-key audit result in the PR description as a checklist signed off by both author and reviewer.
| * <p>Uses {@link ConfigObject#entrySet()} (not {@link Config#entrySet()}) because | ||
| * the latter silently excludes null values, making them impossible to detect. | ||
| */ | ||
| public static Config stripNullLeaves(Config config) { |
There was a problem hiding this comment.
[SHOULD] Consider splitting: stripNullLeaves bug fix and reference.conf removal are independent changes
This PR combines two technically independent changes whose risk profiles differ by an order of magnitude:
-
stripNullLeavesbug fix (~30 lines) — pure bug fix for the startup crash when a user config contains an explicitnullleaf. Self-contained, low risk, does not move the default-value source. Could be cherry-picked into a current release. -
Remove
reference.conf+ introduce BeanDefaults reflection-based fallback (~1900 lines) — pure architectural refactor to eliminate dual-source maintenance. High risk: in the old modelreference.confwas a runtime requirement, so bean field initializers were effectively dead code (always overwritten via the fallback chain). Removingreference.confmakes those initializers live for the first time, and any historical drift becomes a silent user-observable behavior change. Needs default-value parity verification, thepBFT*remap idiom, and confirmation that the shippedconfig.confcarries the operationally critical keys that used to live inreference.conf.
The two changes are loosely coupled: fixing #1 does not require touching reference.conf at all (just call stripNullLeaves on the user-supplied config before merging). Likewise, #2 does not require stripNullLeaves — it's an independent architectural choice.
Merging them together makes both halves harder to review (the small bug fix gets buried in the large refactor's diff), and creates a coupling problem for future rollback decisions: if a user-observable drift surfaces in the architectural part and #2 needs to be reverted, the urgent bug fix in #1 would be reverted with it.
Suggestion: split into PR-A (just stripNullLeaves + call site, keep reference.conf unchanged) and PR-B (BeanDefaults + delete reference.conf + nine fromConfig changes), in that order. Alternatively, if you prefer to keep them together, list the independent logical changes in the PR description with explicit rollback boundaries so a future revert decision can target one without the other.
| @@ -386,10 +374,11 @@ public static NodeConfig fromConfig(Config config) { | |||
| nc.maxConnectionsWithSameIp = section.getInt("maxActiveNodesWithSameIp"); | |||
There was a problem hiding this comment.
[SHOULD] Two legacy-key fallbacks in same method use opposite priority semantics
This method now contains two legacy-key fallback handlers with opposite precedence semantics, which is a cognitive trap for future maintainers:
maxActiveNodesWithSameIp(legacy) →maxConnectionsWithSameIp(modern): legacy unconditionally overrides modern (legacy-wins). Asserted bytestLegacyAliasTakesPriorityOverModernKey.fullNodeAllowShieldedTransaction(legacy) →allowShieldedTransactionApi(modern): modern wins; legacy only used when modern is absent. Asserted bytestShieldedApiModernKeyTakesPriorityOverLegacy.
A developer adding a third legacy-key fallback will have no signal which pattern to follow — and the inconsistency is invisible without reading both tests.
Suggestion: add a header comment to fromConfig documenting the asymmetry and its history (bug-for-bug compatibility for maxActiveNodesWithSameIp, intentional fix for allowShieldedTransactionApi); ideally, plan a follow-up to unify on modern-wins (with deprecation warning on legacy keys) so a single consistent rule applies.
|
[NIT] Three private static helpers are unused
Suggestion: delete the three unused helpers; if they were intentionally reserved for future legacy-key handling, keep them with a brief explanatory comment and |
|
[SHOULD] PR description's "dual-source maintenance" premise is misframed; the real problem is DX, not engineering debt The PR description's "What does this PR do?" and "Why are these changes required? §1 Dual-source maintenance with no compile-time enforcement" frame the core motivation as eliminating a dual-source maintenance problem. This diagnosis does not match the runtime reality of the old model. There is no dual-source maintenance problem:
The actual problem the PR is responding to is DX (developer experience), not engineering debt:
These are real, but they are DX problems. Diagnosing them as "dual-source maintenance / engineering debt" leads to over-treatment: a ~1900-line architectural refactor (delete Lighter alternatives that target the actual DX issue:
Suggestions:
This is the most important review concern in this PR — every other finding (default-value parity, |
| if (confFile.exists()) { | ||
| config = ConfigFactory.parseFile(confFile) | ||
| .withFallback(ConfigFactory.defaultReference()); | ||
| config = ConfigFactory.parseFile(confFile); |
There was a problem hiding this comment.
[MUST] Removing defaultReference() here regresses external partial config files. seed.node.ip.list is still read manually in MiscConfig.fromConfig() and no longer has any fallback on the file-load path, so a minimal -c file now yields an empty seed set instead of the legacy bootstrap defaults. Please keep defaultReference() until the remaining top-level/manual-read domains are migrated, or add an equivalent fallback for them before deleting reference.conf.
|
|
||
| public static GenesisConfig fromConfig(Config config) { | ||
| Config section = config.getConfig("genesis.block"); | ||
| Config defaults = BeanDefaults.toConfig(new GenesisConfig()); |
There was a problem hiding this comment.
[MUST] This is not parity-preserving with the old runtime defaults. reference.conf previously supplied a populated genesis.block, while BeanDefaults.toConfig(new GenesisConfig()) only supplies empty strings/lists. Combined with Args.applyGenesisConfig(), an external partial config that omits genesis.block now falls back to GenesisBlock.getDefault() instead of the shipped genesis config. Please preserve the legacy genesis fallback here, or exclude genesis.block from the reference.conf removal in this PR.
| if (section.hasPath(topicsKey)) { | ||
| // topics: apply per-item BeanDefaults so optional fields (solidified, ethCompatible, | ||
| // redundancy) don't require every item to declare them explicitly. | ||
| if (userSection.hasPath("topics")) { |
There was a problem hiding this comment.
[MUST] This changes the default event.subscribe.topics contract. reference.conf previously provided 7 default topic entries; after this change topics stays empty unless the user writes it explicitly. That breaks parity for external partial config files. Please preserve the legacy default topic list here, or keep reference.conf on the file-load path until event.subscribe is migrated completely.
Conflict resolution (all conflicts are additive — keep both sides): - NodeConfig.java: keep maxBatchSize/maxResponseSize/maxAddressSize (HEAD) and maxMessageSize (develop) - reference.conf: keep all four config entries - Args.java: keep all four PARAMETER assignments Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What does this PR do?
Eliminates
reference.confand makes Java bean field initializers the single source of truth for configuration defaults.Previously, configuration defaults were maintained in two places:
private int maxConnections = 30)common/src/main/resources/reference.conf(HOCON fallback loaded automatically by Typesafe Config)Any new config field or default value change required keeping both in sync.
reference.confwas invisible to most developers and frequently fell out of date.Core change: Introduce
BeanDefaults, a utility that serializes a bean instance's current field values into a TypesafeConfigobject. EachXxxConfig.fromConfig()now callsBeanDefaults.toConfig(new XxxConfig())and uses it as awithFallback(), soConfigBeanFactoryalways finds every key it needs — even when the user'sconfig.confonly sets a subset of fields.Specific changes:
BeanDefaults.java: reflects public getter+setter pairs viaBeanInfo, recurses into nested beans, serializes lists — mirrors exactly whatConfigBeanFactorybindsXxxConfig.fromConfig()methods (BlockConfig,CommitteeConfig,EventConfig,GenesisConfig,MetricsConfig,NodeConfig,RateLimiterConfig,StorageConfig,VmConfig): replace hardconfig.getConfig(section)calls withhasPathguard +withFallback(BeanDefaults.toConfig(...)); all classes now use an explicituserSectionvariable to hold the user's raw values before merging with defaultsreference.conf: no longer needed;BeanDefaultsreplaces its roleConfiguration.java: remove thewithFallback(ConfigFactory.defaultReference())call from the file-based load pathconfig.conf: add previously undocumented parameters that were silently defaulted throughreference.conf(e.g.storage.backup,storage.checkpoint,node.maxFastForwardNum,rate.limiter.p2p.*, etc.)BeanDefaultsTest.java: 16 tests covering default values, round-trip withConfigBeanFactory, and user-value override for all major config beansWhy are these changes required?
1. Dual-source maintenance with no compile-time enforcement
The dual-source maintenance created recurring issues:
reference.conf, with no compile-time enforcement of that requirementreference.confwas not visible to users browsing source code or Java docsreference.confdefaults was only caught at runtimereference.confcould not tell which keys were still active vs. superseded by bean logicAfter this change, the only place a developer needs to touch to add a new config parameter is the Java bean class.
2.
node.discovery.external.ip = nullin reference.conf caused startup failureIf the existed config file contained:
When Typesafe Config merged this with the user's
config.conf, thenullvalue propagated into the merged config.ConfigBeanFactorywould then attempt to bindnullto theStringfield, causing a startup crash even when the user had not set this key at all. This PR introducesBeanDefaults.stripNullLeaves(), which strips all HOCONnullleaves from the user section before merging with bean defaults — so a user's explicitnullentry never overwrites a valid Java default.3.
allowShieldedTransactionApipriority was silently brokenWhen both
node.allowShieldedTransactionApi(modern) andnode.fullNodeAllowShieldedTransaction(legacy) were present inconfig.conf, the modern key should take precedence. The original fallback logic checkedsection.hasPath("allowShieldedTransactionApi"), butsectionis the merged config (user values +BeanDefaults), which always containsallowShieldedTransactionApifrom the defaults — making the condition always true and the entire legacy-key branch dead code. As a result, a user who set onlyfullNodeAllowShieldedTransactionwould silently get the default value instead of their configured value.Fixed by checking
userSection.hasPath()(the raw user-supplied values only) instead ofsection.hasPath(). The correct semantics are now enforced and covered by tests:true)4. Legacy-key fallback pattern relied on an implicit invariant that was easy to break
Before this PR, six
XxxConfig.fromConfig()methods used an inline one-step form:This form does not expose the raw user values as a named variable. Any developer adding a legacy-key fallback (like the
allowShieldedTransactionApicase above) would be tempted to writesection.hasPath()— which checks the merged config and silently makes the fallback dead code. All six classes now use the two-step form with an explicituserSectionvariable:The naming makes the invariant obvious:
userSectionis what the user wrote;sectionis the merged result. Future legacy-key checks must useuserSection.hasPath().This PR has been tested by:
BeanDefaultsTest,NodeConfigTest,BlockConfigTest,VmConfigTest,StorageConfigTest,CommitteeConfigTest,EventConfigTest,GenesisConfigTest,MetricsConfigTest,RateLimiterConfigTest— all pass)