{"record":{"id":"9500cbb07eee21e4","repo":"alibaba/spring-ai-alibaba","slug":"threadid-isn-t-allow-null","errorCode":null,"errorMessage":"threadId isn't allow null","messagePattern":"threadId isn't allow null","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/redis/RedisSaver.java","lineNumber":267,"sourceCode":"\t\t}\n\t\tcatch (InterruptedException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\tcatch (IOException | ClassNotFoundException e) {\n\t\t\tthrow new RuntimeException(\"Failed to deserialize checkpoints\", e);\n\t\t}\n\t\tfinally {\n\t\t\tif (lock.isHeldByCurrentThread()) {\n\t\t\t\tlock.unlock();\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic Optional<Checkpoint> get(RunnableConfig config) {\n\t\tOptional<String> threadNameOpt = config.threadId();\n\t\tif (!threadNameOpt.isPresent()) {\n\t\t\tthrow new IllegalArgumentException(\"threadId isn't allow null\");\n\t\t}\n\n\t\tString threadName = threadNameOpt.get();\n\t\tRLock lock = redisson.getLock(LOCK_PREFIX + threadName);\n\t\tboolean tryLock = false;\n\t\ttry {\n\t\t\t// 500ms timeout for read operations (get)\n\t\t\ttryLock = lock.tryLock(500, TimeUnit.MILLISECONDS);\n\t\t\tif (!tryLock) {\n\t\t\t\treturn Optional.empty();\n\t\t\t}\n\n\t\t\t// Get active thread_id for the thread_name\n\t\t\tString threadId = getActiveThreadId(threadName);\n\t\t\tif (threadId == null) {\n\t\t\t\treturn Optional.empty();\n\t\t\t}\n","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/redis/RedisSaver.java#L249-L285","documentation":"RedisSaver.get(CheckpointSaver.RunnableConfig) requires a threadId in the config; if Optional<String> threadId() is empty it throws IllegalArgumentException('threadId isn\\u2019t allow null') before acquiring the Redis lock. (This method backs checkpoint(...) and retrieved(...) flows.)","triggerScenarios":"Calling saver.get(config) or code paths like checkpoint()/retrieved() with a RunnableConfig built without threadId — e.g. RunnableConfig.builder().build().","commonSituations":"Forgetting threadId when resuming a graph from a checkpoint; configs copied from a fresh invocation that never got an id; tests constructing minimal configs.","solutions":["Always set threadId when building RunnableConfig for checkpoint operations.","Guard calls: if (config.threadId().isEmpty()) throw/skip before invoking get().","Ensure your stateful runnable assigns a threadId at start (e.g. via RunnableConfig with a stable session id)."],"exampleFix":"// before\nOptional<Checkpoint> cp = saver.get(RunnableConfig.builder().build());\n// after\nRunnableConfig config = RunnableConfig.builder().threadId(sessionId).build();\nOptional<Checkpoint> cp = saver.get(config);","handlingStrategy":"validation","validationCode":"if (config == null || config.threadId().isEmpty()) {\n    throw new IllegalStateException(\"RunnableConfig must carry a threadId before get()\");\n}","typeGuard":null,"tryCatchPattern":"try { saver.get(config); } catch (IllegalArgumentException e) { if (e.getMessage().contains(\"threadId\")) throw new IllegalStateException(\"Set threadId on RunnableConfig\", e); throw e; }","preventionTips":["Generate a stable threadId (session id) per conversation","Centralize RunnableConfig construction","Assert threadId presence before any checkpoint API call"],"tags":["redis","checkpoint","argument-validation"],"backgroundTag":"missing-required-argument","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}