{"record":{"id":"dc71d1517678616d","repo":"mybatis/mybatis-3","slug":"couldn-t-get-a-lock-in-timeout-for-the-key-key","errorCode":null,"errorMessage":"Couldn't get a lock in {timeout} for the key {key} at the cache {cacheId}","messagePattern":"Couldn't get a lock in (.+?) for the key (.+?) at the cache (.+?)","errorType":"exception","errorClass":"CacheException","httpStatus":null,"severity":"error","filePath":"src/main/java/org/apache/ibatis/cache/decorators/BlockingCache.java","lineNumber":100,"sourceCode":"  }\n\n  @Override\n  public void clear() {\n    delegate.clear();\n  }\n\n  private void acquireLock(Object key) {\n    CountDownLatch newLatch = new CountDownLatch(1);\n    while (true) {\n      CountDownLatch latch = locks.putIfAbsent(key, newLatch);\n      if (latch == null) {\n        break;\n      }\n      try {\n        if (timeout > 0) {\n          boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);\n          if (!acquired) {\n            throw new CacheException(\n                \"Couldn't get a lock in \" + timeout + \" for the key \" + key + \" at the cache \" + delegate.getId());\n          }\n        } else {\n          latch.await();\n        }\n      } catch (InterruptedException e) {\n        throw new CacheException(\"Got interrupted while trying to acquire lock for key \" + key, e);\n      }\n    }\n  }\n\n  private void releaseLock(Object key) {\n    CountDownLatch latch = locks.remove(key);\n    if (latch == null) {\n      throw new IllegalStateException(\"Detected an attempt at releasing unacquired lock. This should never happen.\");\n    }\n    latch.countDown();\n  }","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/mybatis/mybatis-3/blob/008069adb1b089579b5dcba87ee591908b263274/src/main/java/org/apache/ibatis/cache/decorators/BlockingCache.java#L82-L118","documentation":"Thrown by BlockingCache.acquireLock() when a query for a cache key cannot obtain the per-key CountDownLatch within the configured timeout (the 'blocking' cache decorator serializes queries for the same key so a cache miss is not thundering-herded). The message includes the timeout in ms, the key, and the cache id. timeout > 0 only when the decorator was configured with a positive blockingTimeout.","triggerScenarios":"BlockingCache configured via <cache ... ><property name=\"blockingTimeout\" value=\"250\"/></cache> (or CacheDecorator order in a custom cache), and a query for key K takes longer than 250ms while another thread holds K's latch — e.g. a slow first query on a cold cache with many concurrent requests for the same row.","commonSituations":"Cold-start stampedes on hot keys where the loading query exceeds the blocking timeout; slow DB under load making every first-miss query exceed a small timeout; deadlock-ish long transactions holding the query; timeout set too low (default is 0 = wait forever, so someone set it deliberately).","solutions":["Increase blockingTimeout (or remove the property to wait indefinitely) so typical query latency fits inside the window","Fix the underlying slow query (index, fetch size) so first-miss loads finish before other waiters time out","Pre-warm the cache for hot keys at startup so concurrent waiters never block on a cold miss","Consider whether BlockingCache is needed at all — a request-coalescing layer or shorter TTL may serve better"],"exampleFix":"<!-- before -->\n<cache type=\"org.apache.ibatis.cache.decorators.BlockingCache\" blockingTimeout=\"100\"/>\n\n<!-- after -->\n<cache type=\"org.apache.ibatis.cache.decorators.BlockingCache\" blockingTimeout=\"3000\"/>","handlingStrategy":"retry","validationCode":"// before enabling: measure p99 of the cached query and set blockingTimeout above it\nlong p99 = measureQueryP99Ms();\nif (blockingTimeout > 0 && blockingTimeout < p99) throw new IllegalStateException(\"blockingTimeout \" + blockingTimeout + \"ms < query p99 \" + p99 + \"ms\");","typeGuard":null,"tryCatchPattern":"catch (CacheException e) { if (e.getMessage().contains(\"Couldn't get a lock\")) { backoffAndRetryOnce(key); } else throw e; } — bounded retry only; a genuinely stuck holder needs cache eviction.","preventionTips":["Set blockingTimeout comfortably above worst-case query latency","Pre-warm hot keys to avoid cold-miss stampedes","Reassess whether BlockingCache is the right tool for your load"],"tags":["mybatis","cache","concurrency","timeout"],"backgroundTag":null,"analyzedSha":"008069adb1b089579b5dcba87ee591908b263274","analyzedAt":"2026-08-14T13:07:10.264Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}