Skip to content

Conversation

@Aiden2014
Copy link
Collaborator

@Aiden2014 Aiden2014 commented Sep 15, 2025

koupleless/koupleless#405

Summary by CodeRabbit

  • New Features

    • Cross-pod coordination for business operations using model versions to prevent outdated or unintended uninstalls.
    • Persist business identity after successful install and automatically remove it after uninstall.
    • Install/uninstall now enforce access checks and surface clearer error logging for expired/unauthorized operations.
  • Tests

    • Added comprehensive unit and concurrency tests covering coordination, access control, save/remove behavior, and concurrent scenarios.
  • Chores

    • Version bumped to 1.4.3.

…ties

Add BizOpsPodCoordinator class to handle saving, removing, and accessing biz identities and their model versions. Update InstallBizHandler and UninstallBizHandler to utilize the new coordinator for managing biz identity access and lifecycle.
Also, introduce bizModelVersion in ArkBizMeta for better tracking of biz versions.
- Introduced ArkletLogger to log access control failures.
- Cleaned up unused imports in BizOpsPodCoordinator.
- Added unit tests for BizOpsPodCoordinator functionality.
Improve the access control logic in the canAccess method to handle
various cases for bizModelVersion and bizIdentity. This change
ensures compatibility with older versions and prevents the execution
of outdated commands.

Case 1: Allow access if bizModelVersion is empty.
Case 2: Allow access if no record exists for bizIdentity.
Case 3: Allow access if the version matches the current request.
Only deny access when bizModelVersion is not empty and does not match.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

Adds a pod-level coordinator for biz operations (BizOpsPodCoordinator), integrates it into install/uninstall handlers (post-install save, pre-uninstall access check and post-uninstall cleanup), extends ArkBizMeta with bizModelVersion, adds comprehensive concurrency tests, and bumps root POM revision to 1.4.3.

Changes

Cohort / File(s) Summary
Biz ops coordination utility
arklet-core/src/main/java/.../command/coordinate/BizOpsPodCoordinator.java, arklet-core/src/test/java/.../command/coordinate/BizOpsPodCoordinatorTest.java
Adds a thread-safe in-memory coordinator with save, remove, and canAccess semantics plus extensive unit tests covering normal and concurrent scenarios.
Install/Uninstall handlers integration
arklet-core/src/main/java/.../handler/InstallBizHandler.java, arklet-core/src/main/java/.../handler/UninstallBizHandler.java
Install handler now generates a bizIdentity post-success and calls BizOpsPodCoordinator.save(...). Uninstall handler now computes bizIdentity, checks BizOpsPodCoordinator.canAccess(...) before uninstall, logs and returns failure on denied access, and removes mapping on successful uninstall; UninstallBizHandler now implements ArkBizOps and wraps errors.
Biz metadata extension
arklet-core/src/main/java/.../meta/bizops/ArkBizMeta.java
Adds bizModelVersion field with getter and setter.
Build/versioning
pom.xml
Bumps project revision from 1.4.2-SNAPSHOT to 1.4.3.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant H as InstallBizHandler
  participant S as Biz Operation Service
  participant I as BizIdentityUtils
  participant P as BizOpsPodCoordinator

  C->>H: Install request (bizName, bizVersion, bizModelVersion)
  H->>S: install(...)
  S-->>H: result(success/failure)
  alt success
    H->>I: generateBizIdentity(bizName,bizVersion)
    I-->>H: bizIdentity
    H->>P: save(bizIdentity, bizModelVersion)
    P-->>H: ack
    H-->>C: success
  else failure
    H-->>C: failure
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant H as UninstallBizHandler
  participant I as BizIdentityUtils
  participant P as BizOpsPodCoordinator
  participant S as Biz Operation Service
  participant L as Logger

  C->>H: Uninstall request (bizName, bizVersion, bizModelVersion)
  H->>I: generateBizIdentity(bizName,bizVersion)
  I-->>H: bizIdentity
  H->>P: canAccess(bizIdentity, bizModelVersion)
  alt access denied
    H->>L: log error ("expired/unauthorized")
    H-->>C: failed response
  else access allowed
    H->>S: uninstall(...)
    S-->>H: result(success/failure)
    alt success
      H->>P: remove(bizIdentity, bizModelVersion)
      P-->>H: ack
      H-->>C: success
    else failure
      H-->>C: failure
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • lvjing2
  • inrandom

Poem

A hop, a nibble, code saved in a pod,
I tag each biz as I bless the new prod.
Install confirms, I tuck versions in place,
Uninstall checks—no stale run, no race.
Carrots and logs, 1.4.3—cheers from this rabbit. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the primary intent of the changeset — fixing module instances in the JVM disappearing after pod replacement — and aligns with the key changes (addition of BizOpsPodCoordinator, install/uninstall side-effects, and bizModelVersion access checks). It is concise, specific, and meaningful for a teammate scanning commit history. There is no indication the title is misleading or unrelated to the diff.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java (2)

32-32: New field added; document null/empty semantics.

Please clarify whether null and "" are equivalent on the wire for bizModelVersion (align with BizOpsPodCoordinator which normalizes empty to ""). Add brief Javadoc note.


89-105: Accessor LGTM; consider @nullable hint.

If bizModelVersion can be absent from older clients, annotate getter/setter with @Nullable (or equivalent) to make intent explicit.

arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java (1)

74-77: Post‑success save works; consider pre‑save with rollback to shrink race window.

Tiny gap exists between install success and save(...) where a legacy uninstall could sneak in. Option: pre‑save, then rollback mapping on failure.

Example:

-InstallBizClientResponse r = convertClientResponse(getOperationService().install(convertInstallRequest(input)));
-...
-if (ResponseCode.SUCCESS.equals(r.getCode())) {
-    BizOpsPodCoordinator.save(bizIdentity, bizModelVersion);
+// pre-save to gate stale commands, then rollback if install fails
+BizOpsPodCoordinator.save(bizIdentity, bizModelVersion);
+InstallBizClientResponse r = convertClientResponse(getOperationService().install(convertInstallRequest(input)));
+...
+if (ResponseCode.SUCCESS.equals(r.getCode())) {
     return Output.ofSuccess(r);
 } else {
+    BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion);
     return Output.ofFailed(r, "install biz not success!");
 }
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (2)

79-88: Micro: avoid double map get; consider stricter empty‑version policy.

  • Read once for consistency.
  • Optional: if recorded version is non‑empty and request is empty, deny (behind a compat flag) to harden against stale commands.

Apply:

 public static boolean canAccess(String bizIdentity, String bizModelVersion) {
-    return StringUtils.isEmpty(bizModelVersion)
-            || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity))
-            || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion);
+    final String recorded = bizIdentityLockMap.get(bizIdentity);
+    if (StringUtils.isEmpty(bizModelVersion)) {
+        // compatibility: allow only if no recorded version
+        return StringUtils.isEmpty(recorded);
+    }
+    return StringUtils.isEmpty(recorded) || recorded.equals(bizModelVersion);
 }

33-41: Scope note: coordinator is per‑JVM; cross‑pod/state loss on restart.

Confirm this meets the intended “pod‑level” guard only. If cross‑pod protection is desired, persist to shared store (e.g., ConfigMap/Redis) or propagate via controller.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3edfa91 and e6aee62.

📒 Files selected for processing (6)
  • arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java (6 hunks)
  • arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java (3 hunks)
  • arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1 hunks)
  • arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java (2 hunks)
  • arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java (1 hunks)
  • pom.xml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java (1)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1)
  • BizOpsPodCoordinator (33-89)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java (5)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1)
  • BizOpsPodCoordinator (33-89)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/Output.java (1)
  • Output (28-152)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/meta/bizops/ArkBizMeta.java (1)
  • ArkBizMeta (28-124)
koupleless-common/src/main/java/com/alipay/sofa/koupleless/common/log/ArkletLogger.java (1)
  • ArkletLogger (29-407)
koupleless-common/src/main/java/com/alipay/sofa/koupleless/common/log/ArkletLoggerFactory.java (1)
  • ArkletLoggerFactory (28-76)
arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java (1)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1)
  • BizOpsPodCoordinator (33-89)
🔇 Additional comments (12)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/InstallBizHandler.java (2)

59-61: Implements ArkBizOps — good alignment.


88-90: Formatting-only changes — OK.

pom.xml (1)

12-12: Version bump OK; verify no hard‑pinned child versions remain.
rg returned "No files were searched". From the repository root re-run and paste outputs:
git grep -n -E '\s1.4.[^<]' || true
git grep -n -F '1.4.2' || true
Fallback (if git grep unavailable):
rg -n --hidden -uu '\s1.4.[^<]' -g '!/target/'
rg -n --hidden -uu '1.4.2' -g '!/target/'

arklet-core/src/test/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinatorTest.java (9)

1-38: LGTM! Well-structured test class with proper documentation.

The Apache license header, imports, and class structure are appropriate. The Javadoc provides clear context about the test purpose.


40-61: Proper test isolation using reflection to clear static state.

The setUp/tearDown methods correctly use reflection to clear the static bizIdentityLockMap to ensure test isolation. This is a good practice when testing classes with static state.


66-89: LGTM! Good test coverage for normal save operation.

Both test methods properly validate the save functionality with realistic business identifiers and use canAccess to verify the saved state. The test data formats match real-world scenarios.


94-118: LGTM! Comprehensive null and empty string handling tests.

These tests properly verify that the coordinator handles null and empty bizModelVersion values correctly, converting them to empty strings as expected by the implementation.


123-137: LGTM! Override behavior correctly tested.

The test properly validates that newer versions overwrite older ones and that access control works correctly after the update.


142-193: LGTM! Comprehensive remove operation testing.

All three remove scenarios are well-tested:

  • Normal removal and verification of subsequent access behavior
  • Attempting to remove non-existent entries (should not throw exceptions)
  • Version mismatch removal (should not affect existing entries)

The test logic correctly validates the expected behavior based on the canAccess implementation.


198-297: LGTM! Thorough canAccess method testing.

Excellent coverage of all canAccess scenarios:

  • Null/empty bizModelVersion handling
  • Different business identity versions
  • Empty values in the map
  • Version matching and mismatching
  • Non-existent business identities

All assertions align with the coordinator's documented behavior.


302-339: LGTM! Comprehensive concurrency stress test.

The test properly validates thread safety with:

  • 50 threads performing 100 operations each
  • Proper use of CountDownLatch for synchronization
  • Appropriate timeout values (30s for operations, 5s for cleanup)
  • Verification that no exceptions are thrown during concurrent access

The test design effectively stresses the ConcurrentHashMap implementation.


344-378: LGTM! Multi-identity concurrency test validates isolation.

This test effectively validates that concurrent operations on different business identities don't interfere with each other. The assertions within the executor threads properly verify version updates and access control behavior.

Comment on lines 52 to 60
String bizIdentity = BizIdentityUtils.generateBizIdentity(input.getBizName(), input.getBizVersion());
String bizModelVersion = input.getBizModelVersion();
if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) {
LOGGER.error(
"can not access biz because the command is expired. bizIdentity: {}, bizModelVersion: {}",
bizIdentity, bizModelVersion);
return Output.ofFailed("can not access biz because the command is expired");
}
ClientResponse res = getOperationService().uninstall(input.getBizName(), input.getBizVersion());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Access check allows empty bizModelVersion — consider stricter rule behind a flag.

Current logic permits uninstall when request version is empty, even if a non‑empty version is recorded; this weakens protection against stale commands. Suggest denying when map has a non‑empty version and request is empty, gated by a compat flag.

Proposed guard:

-if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) {
+if (!BizOpsPodCoordinator.canAccess(bizIdentity, bizModelVersion)) {
     ...
 }

and inside coordinator (see separate comment) refine empty‑version handling or make it configurable.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/builtin/handler/UninstallBizHandler.java
around lines 52 to 60, the current access check allows an empty bizModelVersion
in the request to pass when a non-empty version is recorded, weakening
protection; update the guard to deny access when the stored coordinator version
is non-empty and the request bizModelVersion is empty, unless a
backward-compatibility flag is enabled. Implement this by: 1) querying
BizOpsPodCoordinator for the recorded version for bizIdentity before the
canAccess call, 2) if recordedVersion is non-empty and
input.getBizModelVersion() is empty, return Output.ofFailed(...) (or call
canAccess only after checking the compat flag), and 3) add a feature
flag/compatibility toggle (checked here or via BizOpsPodCoordinator) to allow
the old behavior when explicitly enabled.

Comment on lines 61 to 63
if (ResponseCode.SUCCESS.equals(res.getCode())) {
BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion);
return Output.ofSuccess(res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Possible NPE on remove when bizModelVersion is null (ConcurrentHashMap disallows null value).

BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion) will throw if bizModelVersion == null (legacy clients). Fix in coordinator by normalizing null/empty before calling Map.remove.

Apply coordinator fix shown in that file’s comment; alternatively, add a local guard:

- BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion);
+ if (!StringUtils.isEmpty(bizModelVersion)) {
+     BizOpsPodCoordinator.remove(bizIdentity, bizModelVersion);
+ } else {
+     // legacy client: best-effort cleanup; safe to remove by key
+     BizOpsPodCoordinator.remove(bizIdentity, StringUtils.EMPTY_STRING);
+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 50 to 55
public static void save(String bizIdentity, String bizModelVersion) {
if (StringUtils.isEmpty(bizModelVersion)) {
bizModelVersion = StringUtils.EMPTY_STRING;
}
bizIdentityLockMap.put(bizIdentity, bizModelVersion);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate bizIdentity to avoid CHM NPE; normalize inputs.

ConcurrentHashMap disallows null keys; add a guard for bizIdentity and normalize bizModelVersion eagerly.

Apply:

 public static void save(String bizIdentity, String bizModelVersion) {
+    if (StringUtils.isEmpty(bizIdentity)) {
+        // ignore invalid key or consider throwing IllegalArgumentException
+        return;
+    }
     if (StringUtils.isEmpty(bizModelVersion)) {
         bizModelVersion = StringUtils.EMPTY_STRING;
     }
     bizIdentityLockMap.put(bizIdentity, bizModelVersion);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void save(String bizIdentity, String bizModelVersion) {
if (StringUtils.isEmpty(bizModelVersion)) {
bizModelVersion = StringUtils.EMPTY_STRING;
}
bizIdentityLockMap.put(bizIdentity, bizModelVersion);
}
public static void save(String bizIdentity, String bizModelVersion) {
if (StringUtils.isEmpty(bizIdentity)) {
// ignore invalid key or consider throwing IllegalArgumentException
return;
}
if (StringUtils.isEmpty(bizModelVersion)) {
bizModelVersion = StringUtils.EMPTY_STRING;
}
bizIdentityLockMap.put(bizIdentity, bizModelVersion);
}
🤖 Prompt for AI Agents
In
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java
around lines 50 to 55, the method saves into a ConcurrentHashMap without
validating the key which can cause a NullPointerException; update the method to
first validate bizIdentity (return or throw IllegalArgumentException if
null/blank) and normalize bizModelVersion eagerly (replace null with
StringUtils.EMPTY_STRING and trim it), then perform the put into
bizIdentityLockMap only with the validated non-null key and normalized value.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (2)

50-58: Resolved: save() now validates key and normalizes value — LGTM.

This addresses the earlier CHM null-key and normalization concern.


69-78: Resolved: remove() handles null/empty version without NPE — LGTM.

Branching to remove by key for empty version fixes the previous remove(key, null) issue.

🧹 Nitpick comments (4)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (4)

33-34: Make utility class non‑instantiable.

Prevent accidental instantiation.

 public class BizOpsPodCoordinator {
 
+    private BizOpsPodCoordinator() {
+        // no-op
+    }

91-99: Minor: avoid potential typo in comment ("arktcl").

Should this be "arkctl"? Please confirm and adjust.


35-40: Name nit: bizIdentityLockMap is not a lock.

Consider a clearer name like bizModelVersionByBizIdentity.


80-101: Add denial observability.

When canAccess() returns false, consider emitting a debug log with bizIdentity and versions for incident triage.

I can draft a minimal logging change if desired.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6aee62 and b2b3508.

📒 Files selected for processing (1)
  • arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1 hunks)
🔇 Additional comments (1)
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (1)

90-101: Add tests for null identity and concurrent-remove race

  • canAccess(null, non-empty) must not throw; add a test asserting the chosen policy (allow or deny) explicitly.
  • canAccess must tolerate the map entry being removed between reads — add a concurrent test that removes the entry mid-check to ensure no NPE/race.

Location: arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java (lines 90–101).

Comment on lines 90 to 101
public static boolean canAccess(String bizIdentity, String bizModelVersion) {
// 判断逻辑说明:
// Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl,
// pod-not-exist 和 pod 紧急删除场景)
// Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带
// BizModelVersion,卸载时带上 BizModelVersion)
// Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问
// 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行)
return StringUtils.isEmpty(bizModelVersion)
|| StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity))
|| bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Fix NPE risks in canAccess: null key to CHM and TOCTOU on double get.

  • If bizIdentity is null/empty, ConcurrentHashMap.get(null) throws.
  • Two consecutive get calls can race with a concurrent remove(), making the second equals dereference NPE.

Proposed fix: guard bizIdentity and read the map once; compare safely.

-    public static boolean canAccess(String bizIdentity, String bizModelVersion) {
-        // 判断逻辑说明:
-        // Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl,
-        // pod-not-exist 和 pod 紧急删除场景)
-        // Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带
-        // BizModelVersion,卸载时带上 BizModelVersion)
-        // Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问
-        // 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行)
-        return StringUtils.isEmpty(bizModelVersion)
-                || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity))
-                || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion);
-    }
+    public static boolean canAccess(String bizIdentity, String bizModelVersion) {
+        // Case 1: legacy / compatibility
+        if (StringUtils.isEmpty(bizModelVersion)) {
+            return true;
+        }
+        // Guard against null/blank identity (avoid CHM NPE). Choose safer default: deny.
+        if (StringUtils.isEmpty(bizIdentity)) {
+            return false;
+        }
+        final String recorded = bizIdentityLockMap.get(bizIdentity);
+        // Case 2: no record or empty record -> allow
+        // Case 3: exact match -> allow
+        return StringUtils.isEmpty(recorded) || bizModelVersion.equals(recorded);
+    }

Optional: if you prefer permissive behavior for blank bizIdentity, return true instead — please confirm desired policy.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java"

echo "PWD: $(pwd)"
if [ -f "$file" ]; then
  echo "=== FOUND: $file ==="
  nl -ba "$file" | sed -n '1,260p'
else
  echo "=== FILE NOT FOUND: $file ==="
  echo "Searching repo for class / filename..."
  rg -n --hidden --glob '!**/target/**' 'BizOpsPodCoordinator' -S || true
  rg -n --hidden --glob '!**/target/**' 'BizOpsPodCoordinator.java' -S || true
fi

echo
echo "=== Search for bizIdentityLockMap declaration/usages ==="
rg -n --hidden --glob '!**/target/**' 'bizIdentityLockMap' -S || true

echo
echo "=== Search for canAccess(...) call sites ==="
rg -n --hidden --glob '!**/target/**' 'canAccess\(' -S || true

Length of output: 189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java"

echo "PWD: $(pwd)"
if [ -f "$file" ]; then
  echo "=== FOUND: $file ==="
  sed -n '1,240p' "$file"
else
  echo "=== FILE NOT FOUND: $file ==="
  echo "Searching for BizOpsPodCoordinator..."
  rg -n --hidden --glob '!**/target/**' 'class\s+BizOpsPodCoordinator' -S || true
fi

echo
echo "=== Search for bizIdentityLockMap declaration/usages ==="
rg -n --hidden --glob '!**/target/**' 'bizIdentityLockMap' -S || true

echo
echo "=== Search for canAccess(...) call sites ==="
rg -n --hidden --glob '!**/target/**' 'canAccess\(' -S || true

Length of output: 3944


Fix NPE & TOCTOU in canAccess — guard null identity and read map once

ConcurrentHashMap.get(null) throws NPE; calling get() twice races with concurrent remove() and can cause a .equals(...) NPE. Read the map once and validate bizIdentity before lookup.

-    public static boolean canAccess(String bizIdentity, String bizModelVersion) {
-        // 判断逻辑说明:
-        // Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl,
-        // pod-not-exist 和 pod 紧急删除场景)
-        // Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带
-        // BizModelVersion,卸载时带上 BizModelVersion)
-        // Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问
-        // 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行)
-        return StringUtils.isEmpty(bizModelVersion)
-                || StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity))
-                || bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion);
-    }
+    public static boolean canAccess(String bizIdentity, String bizModelVersion) {
+        // Case 1: legacy / compatibility
+        if (StringUtils.isEmpty(bizModelVersion)) {
+            return true;
+        }
+        // Guard against null/blank identity (avoid CHM NPE). Choose safer default: deny.
+        if (StringUtils.isEmpty(bizIdentity)) {
+            return false;
+        }
+        final String recorded = bizIdentityLockMap.get(bizIdentity);
+        // Case 2: no record or empty record -> allow
+        // Case 3: exact match -> allow
+        return StringUtils.isEmpty(recorded) || bizModelVersion.equals(recorded);
+    }

If permissive behavior for blank bizIdentity is preferred, change the blank-identity branch to return true — confirm policy.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static boolean canAccess(String bizIdentity, String bizModelVersion) {
// 判断逻辑说明:
// Case 1: bizModelVersion 为空 - 兼容性处理,允许访问(兼容旧版本 module-controller,arktcl,
// pod-not-exist 和 pod 紧急删除场景)
// Case 2: bizIdentityLockMap 中没有该 bizIdentity 的记录,允许访问(安装时不带
// BizModelVersion,卸载时带上 BizModelVersion)
// Case 3: bizIdentityLockMap 中的版本与当前请求的版本匹配 - 版本一致,确认卸载的是该 Biz,允许访问
// 只有当 bizModelVersion 不为空且存在 bizModelVersion 且不匹配时,才拒绝访问(防止旧的卸载命令执行)
return StringUtils.isEmpty(bizModelVersion)
|| StringUtils.isEmpty(bizIdentityLockMap.get(bizIdentity))
|| bizIdentityLockMap.get(bizIdentity).equals(bizModelVersion);
}
public static boolean canAccess(String bizIdentity, String bizModelVersion) {
// Case 1: legacy / compatibility
if (StringUtils.isEmpty(bizModelVersion)) {
return true;
}
// Guard against null/blank identity (avoid CHM NPE). Choose safer default: deny.
if (StringUtils.isEmpty(bizIdentity)) {
return false;
}
final String recorded = bizIdentityLockMap.get(bizIdentity);
// Case 2: no record or empty record -> allow
// Case 3: exact match -> allow
return StringUtils.isEmpty(recorded) || bizModelVersion.equals(recorded);
}
🤖 Prompt for AI Agents
In
arklet-core/src/main/java/com/alipay/sofa/koupleless/arklet/core/command/coordinate/BizOpsPodCoordinator.java
around lines 90 to 101, canAccess currently calls
bizIdentityLockMap.get(bizIdentity) twice and will NPE if bizIdentity is null
and can cause a TOCTOU .equals NPE when the map entry is removed concurrently;
fix by first validating bizIdentity (guard against null/blank according to
policy — if blank identity should be allowed, return true), then read
bizIdentityLockMap.get(bizIdentity) once into a local variable and use that
local for the emptiness check and equals comparison so no double lookup or
race-induced NPE occurs.

@codecov
Copy link

codecov bot commented Sep 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.27%. Comparing base (3edfa91) to head (55f2fb7).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #247      +/-   ##
============================================
+ Coverage     73.20%   73.27%   +0.07%     
+ Complexity      196      195       -1     
============================================
  Files           105      105              
  Lines          2668     2668              
  Branches        285      285              
============================================
+ Hits           1953     1955       +2     
+ Misses          573      570       -3     
- Partials        142      143       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Derrickers Derrickers merged commit b34f975 into koupleless:main Sep 23, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants