-
Notifications
You must be signed in to change notification settings - Fork 0
feat:Optimize the event service #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: pr-6192-base
Are you sure you want to change the base?
Changes from all commits
0640dd6
fb2f371
bbc51c6
70a7979
ff44e09
d70cb52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,7 @@ public SolidityTrigger() { | |||||
| @Override | ||||||
| public String toString() { | ||||||
| return new StringBuilder().append("triggerName: ").append(getTriggerName()) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 The change in
Suggested change
|
||||||
| .append("timestamp: ") | ||||||
| .append(", timestamp: ") | ||||||
| .append(timeStamp) | ||||||
| .append(", latestSolidifiedBlockNumber: ") | ||||||
| .append(latestSolidifiedBlockNumber).toString(); | ||||||
|
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High: The introduction of version-dependent conditional logic within core |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -241,6 +241,7 @@ public class Manager { | |||||||||||||||||||||||||||||||||||||||||||||||
| Collections.synchronizedList(Lists.newArrayList()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // the capacity is equal to Integer.MAX_VALUE default | ||||||||||||||||||||||||||||||||||||||||||||||||
| private BlockingQueue<TransactionCapsule> rePushTransactions; | ||||||||||||||||||||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||||||||||||||||||||
| private BlockingQueue<TriggerCapsule> triggerCapsuleQueue; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // log filter | ||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isRunFilterProcessThread = true; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1111,7 +1112,9 @@ private void switchFork(BlockCapsule newHead) | |||||||||||||||||||||||||||||||||||||||||||||||
| while (!getDynamicPropertiesStore() | ||||||||||||||||||||||||||||||||||||||||||||||||
| .getLatestBlockHeaderHash() | ||||||||||||||||||||||||||||||||||||||||||||||||
| .equals(binaryTree.getValue().peekLast().getParentHash())) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| reOrgContractTrigger(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (EventPluginLoader.getInstance().getVersion() == 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
1113
to
+1115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High: This conditional logic introduces a dependency on the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| reOrgContractTrigger(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| reOrgLogsFilter(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| eraseBlock(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1373,11 +1376,26 @@ public void pushBlock(final BlockCapsule block) | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| void blockTrigger(final BlockCapsule block, long oldSolid, long newSolid) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // post block and logs for jsonrpc | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (CommonParameter.getInstance().isJsonRpcHttpFullNodeEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| postBlockFilter(block, false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| postLogsFilter(block, false, false); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
1376
to
+1383
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High: Similar to the above, embedding version-dependent logic directly in
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (CommonParameter.getInstance().isJsonRpcHttpSolidityNodeEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: The removal of
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| postSolidityFilter(oldSolid, newSolid); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (EventPluginLoader.getInstance().getVersion() != 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| lastUsedSolidityNum = newSolid; | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // if event subscribe is enabled, post block trigger to queue | ||||||||||||||||||||||||||||||||||||||||||||||||
| postBlockTrigger(block); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // if event subscribe is enabled, post solidity trigger to queue | ||||||||||||||||||||||||||||||||||||||||||||||||
| postSolidityTrigger(oldSolid, newSolid); | ||||||||||||||||||||||||||||||||||||||||||||||||
| postSolidityTrigger(newSolid); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error("Block trigger failed. head: {}, oldSolid: {}, newSolid: {}", | ||||||||||||||||||||||||||||||||||||||||||||||||
| block.getNum(), oldSolid, newSolid, e); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1517,7 +1535,8 @@ public TransactionInfo processTransaction(final TransactionCapsule trxCap, Block | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // if event subscribe is enabled, post contract triggers to queue | ||||||||||||||||||||||||||||||||||||||||||||||||
| // only trigger when process block | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (Objects.nonNull(blockCap) && !blockCap.isMerkleRootEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (Objects.nonNull(blockCap) && !blockCap.isMerkleRootEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||
| && EventPluginLoader.getInstance().getVersion() == 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| String blockHash = blockCap.getBlockId().toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| postContractTrigger(trace, false, blockHash); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2096,7 +2115,7 @@ private void postSolidityFilter(final long oldSolidNum, final long latestSolidif | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: Consistent with the previous change where
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private void postSolidityTrigger(final long oldSolidNum, final long latestSolidifiedBlockNumber) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| private void postSolidityTrigger(final long latestSolidifiedBlockNumber) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (eventPluginLoaded && EventPluginLoader.getInstance().isSolidityLogTriggerEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (Long i : Args.getSolidityContractLogTriggerMap().keySet()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| postSolidityLogContractTrigger(i, latestSolidifiedBlockNumber); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2122,10 +2141,6 @@ private void postSolidityTrigger(final long oldSolidNum, final long latestSolidi | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (CommonParameter.getInstance().isJsonRpcHttpSolidityNodeEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| postSolidityFilter(oldSolidNum, latestSolidifiedBlockNumber); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| lastUsedSolidityNum = latestSolidifiedBlockNumber; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2237,12 +2252,6 @@ private void postLogsFilter(final BlockCapsule blockCapsule, boolean solidified, | |||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: This is a direct consequence of moving these calls to
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| void postBlockTrigger(final BlockCapsule blockCapsule) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // post block and logs for jsonrpc | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (CommonParameter.getInstance().isJsonRpcHttpFullNodeEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| postBlockFilter(blockCapsule, false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| postLogsFilter(blockCapsule, false, false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // process block trigger | ||||||||||||||||||||||||||||||||||||||||||||||||
| long solidityBlkNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (eventPluginLoaded && EventPluginLoader.getInstance().isBlockLogTriggerEnable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 `BlockEventCache` is implemented as a global singleton with static fields. While the use of `ConcurrentHashMap` helps with individual map operations, the methods `add`, `remove`, and `init` perform multiple operations across different fields (`solidNum`, `head`, `solidId`, `blockEventMap`, `numMap`) that are not atomic as a whole.
For example, the The current implementation seems to rely on callers ( |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,96 @@ | ||||||||||||||||||||||||||||||||||||||||
| package org.tron.core.services.event; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import com.google.common.collect.Lists; | ||||||||||||||||||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||
| import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||||||||||||||||||||||||||||
| import lombok.Getter; | ||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||
| import org.tron.core.capsule.BlockCapsule; | ||||||||||||||||||||||||||||||||||||||||
| import org.tron.core.services.event.bo.BlockEvent; | ||||||||||||||||||||||||||||||||||||||||
| import org.tron.core.services.event.exception.EventException; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @Slf4j(topic = "event") | ||||||||||||||||||||||||||||||||||||||||
| public class BlockEventCache { | ||||||||||||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||||||||||||
| private static volatile long solidNum; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||||||||||||
| private static volatile BlockEvent head; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||||||||||||
| private static volatile BlockCapsule.BlockId solidId; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private static Map<BlockCapsule.BlockId, BlockEvent> blockEventMap = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 The reliance on `static` mutable state (`blockEventMap`, `numMap`, `head`, `solidId`) complicates testing and isolation.
More importantly, while For example:
While Suggestion: |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private static Map<Long, List<BlockEvent>> numMap = new ConcurrentHashMap<>(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public static BlockEvent getBlockEvent(BlockCapsule.BlockId blockId) { | ||||||||||||||||||||||||||||||||||||||||
| return blockEventMap.get(blockId); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public static void init(BlockCapsule.BlockId blockId) { | ||||||||||||||||||||||||||||||||||||||||
| blockEventMap.clear(); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium: Clearing
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| numMap.clear(); | ||||||||||||||||||||||||||||||||||||||||
| solidNum = blockId.getNum(); | ||||||||||||||||||||||||||||||||||||||||
| head = new BlockEvent(blockId); | ||||||||||||||||||||||||||||||||||||||||
| solidId = blockId; | ||||||||||||||||||||||||||||||||||||||||
| List<BlockEvent> list = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||
| list.add(head); | ||||||||||||||||||||||||||||||||||||||||
| numMap.put(blockId.getNum(), list); | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The In the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| blockEventMap.put(blockId, head); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low: This is a minor log message redundancy.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| public static void add(BlockEvent blockEvent) throws EventException { | ||||||||||||||||||||||||||||||||||||||||
| logger.info("Add block event, {}", blockEvent.getBlockId().getString(), | ||||||||||||||||||||||||||||||||||||||||
| blockEvent.getParentId().getString()); | ||||||||||||||||||||||||||||||||||||||||
| if (blockEventMap.get(blockEvent.getParentId()) == null) { | ||||||||||||||||||||||||||||||||||||||||
| throw new EventException("unlink BlockEvent, " | ||||||||||||||||||||||||||||||||||||||||
| + blockEvent.getBlockId().getString() + ", " | ||||||||||||||||||||||||||||||||||||||||
| + blockEvent.getParentId().getString()); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The For example:
The entire
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| long num = blockEvent.getBlockId().getNum(); | ||||||||||||||||||||||||||||||||||||||||
| List<BlockEvent> list = numMap.get(num); | ||||||||||||||||||||||||||||||||||||||||
| if (list == null) { | ||||||||||||||||||||||||||||||||||||||||
| list = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||
| numMap.put(num, list); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| list.add(blockEvent); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| blockEventMap.put(blockEvent.getBlockId(), blockEvent); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (num > head.getBlockId().getNum()) { | ||||||||||||||||||||||||||||||||||||||||
| head = blockEvent; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (blockEvent.getSolidId().getNum() > solidId.getNum()) { | ||||||||||||||||||||||||||||||||||||||||
| solidId = blockEvent.getSolidId(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public static void remove(BlockCapsule.BlockId solidId) { | ||||||||||||||||||||||||||||||||||||||||
| logger.info("Remove solidId {}, solidNum {}, {}, {}", | ||||||||||||||||||||||||||||||||||||||||
| solidId.getString(), solidNum, numMap.size(), blockEventMap.size()); | ||||||||||||||||||||||||||||||||||||||||
| numMap.forEach((k, v) -> { | ||||||||||||||||||||||||||||||||||||||||
| if (k < solidId.getNum()) { | ||||||||||||||||||||||||||||||||||||||||
| v.forEach(value -> blockEventMap.remove(value.getBlockId())); | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Removing from a A safer approach is to collect the keys to be removed in a separate list and then iterate over that list to remove the entries from the map.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| numMap.remove(k); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| solidNum = solidId.getNum(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public static List<BlockEvent> getSolidBlockEvents(BlockCapsule.BlockId solidId) { | ||||||||||||||||||||||||||||||||||||||||
| List<BlockEvent> blockEvents = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||
| BlockCapsule.BlockId tmp = solidId; | ||||||||||||||||||||||||||||||||||||||||
| while (tmp.getNum() > solidNum) { | ||||||||||||||||||||||||||||||||||||||||
| BlockEvent blockEvent = blockEventMap.get(tmp); | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The To ensure thread safety, this method should also be synchronized.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| blockEvents.add(blockEvent); | ||||||||||||||||||||||||||||||||||||||||
| tmp = blockEvent.getParentId(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return Lists.reverse(blockEvents); | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 The change in
toString()methods improves readability and consistency.