Redis Lock Collision Returns Fake Success, Triggering NullPointerException

A distributed Redis lock collision returns a default success response with null data, causing client-side NullPointerException when unpacking; the article analyzes the root cause, shows the flawed code, and provides fixes for both server and client sides.

IT Services Circle
IT Services Circle
IT Services Circle
Redis Lock Collision Returns Fake Success, Triggering NullPointerException

1. Scenario Introduction

The business scenario involves two systems pulling the same file type concurrently:

System A : Expected puller, scheduled job calling the interface to save files locally.

System B : Another scheduled job that, due to a developer mistake, also pulls the same file type at the same time window.

Business Service : Provides the pull interface; deployed on two nodes sharing a Redis lock to prevent duplicate pulls.

Downstream : Actual data source (DB/ERP).

Business expectation: only System A should pull. When System B's schedule overlaps, it creates two concurrent requests for the same resource.

2. Log Phenomenon Analysis

Logs from the two sides don't match:

Business Service : Shows request received, downstream called, file generated — appears normal.

System A Client : Occasionally reports get data file has exception : null.

Strangely, HTTP status is not a failure code. The client treats the response as success and attempts to unpack the file list, but hits a NullPointerException .

Key Insight : NullPointerException often has no message, so printing e.getLocalizedMessage() yields the literal string null — looking like "exception has no content", but it's actually an NPE.

3. Root Cause: Concurrent Pull by System A and System B

The sequence:

Same time window
    │
    ├─ System B ──► Business Service ──► Acquires Redis lock
    │                │                    Calls downstream, builds file
    │                │                    Returns: success + data ✔
    │
    └─ System A ──► Business Service ──► Lock still held (collision)
                     │                    Returns: success + result=null
                     │                    Client treats as success → NPE ✖

Timeline:

Time ──►

System A:      Send request ────────────────────────► Get file
Business Service:   [Acquire lock]====Processing====[Release lock]
System B:               Send request (same window)
                         │
                         ▼
                   Collision → Empty success → Unpack NPE

Log correlation:

First request (System B or first to acquire lock): Business service logs show downstream call present, data produced.

Second request (System A or later arrival): Business service logs show only "received pull request", no downstream call.

The lock is shared across the cluster and guards "same business type must not pull concurrently" — it does not distinguish between System A and System B. From the lock's perspective: first come gets processed, later ones are blocked — blocking is expected; but returning a fake success after blocking is not .

4. Code Pitfall: Collision Response Looks Like Success

Business service logic (simplified):

JSONMessageResponse result = new JSONMessageResponse();
String lock = redis.get("LOCK-" + bizType);

if (lock == null) {
    redis.set("LOCK-" + bizType, "1", ttl);
    // Call downstream, build file, setResult(...)
} else {
    return result;  // Collision: direct return
}

Many assume new JSONMessageResponse() = "empty failure object" because no NPE occurs. However, the parent class JSONResponse defaults:

private String code = "success";  // Default is success
private Object result;            // Default null

So on collision, the actual response is: { "code": "success", "result": null } While a true success expects:

{
  "code": "success",
  "result": { "form": { "a.txt": "..." } }
}

Comparison of true success vs collision response:

HTTP : Both 200

code : Both "success" (collision uses default)

result : True success has File Map; collision has null

Failure path did not set code to fail, yet reused the success default. To System A, the response looks "successful", but unpacking explodes — the direct cause of the NPE.

5. NullPointerException Source Code

Client-side (System A) typical code:

JSONObject body = response.getBody();

if (!"success".equals(body.getString("code"))) {
    log.error("failed");
    // Note: some implementations don't return here
}

FormData data = body.getObject("result", FormData.class);
for (String name : data.getForm().keySet()) {  // Explodes here
    // Write local file
}

Step-by-step:

body.code == "success"   → Treated as success, continue
body.result == null      → After deserialization, data == null
data.getForm()           → NullPointerException
Print getLocalizedMessage() → Log shows only ": null"

Summary diagram:

System A (expected) ──► success + form  ──► Write disk ✔

System B (shouldn't pull)─► success + null ──► getForm() NPE ✖
                                 ▲
                                 └── new default code=success

Two issues combined to create the "success yet NPE" failure mode:

Scheduling issue : System B shouldn't pull at this time, yet ran concurrently with System A.

Contract issue : Collision should explicitly fail, but returned fake success; client lacked payload validation.

Missing either would prevent this failure mode.

6. How to Fix the Code

6.1 Server Side: Fail Explicitly on Failure

if (lock != null) {
    return JSONMessageResponse.getFailInstance("lock busy");
    // Or "locked" / "processing" — never success + null result
}

Remember: new objects with default success must not be used directly as failure returns.

6.2 Client Side: Success Code Isn't Enough, Validate Payload

FormData data = body.getObject("result", FormData.class);
if (data == null || data.getForm() == null) {
    // Treat as failure or "no data", don't proceed to loop
    return;
}

One-Sentence Takeaway

System B shouldn't pull but collided with System A → Hit Redis lock; lock blocked re-entry as designed, yet interface returned fake success , caller unpacked into NPE.

Scheduler: Ensure unique puller per resource, separate time windows.

API Writer: Make failure paths explicitly fail.

Caller: Beyond success code, validate payload.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDistributed SystemsJavaconcurrencyRedistroubleshootingdistributed lockNullPointerException
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.