{"record":{"id":"ee68b07004da457b","repo":"alibaba/spring-ai-alibaba","slug":"threadid-is-not-allow-null-ee68b0","errorCode":null,"errorMessage":"threadId is not allow null","messagePattern":"threadId is not 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":227,"sourceCode":"\tprivate String getActiveThreadId(String threadName) {\n\t\tString metaKey = THREAD_META_PREFIX + threadName;\n\t\tRMap<String, String> meta = redisson.getMap(metaKey);\n\n\t\tString threadId = meta.get(FIELD_THREAD_ID);\n\t\tString isReleased = meta.get(FIELD_IS_RELEASED);\n\n\t\tif (threadId != null && !\"true\".equals(isReleased)) {\n\t\t\treturn threadId;\n\t\t}\n\n\t\treturn null; // No active thread exists\n\t}\n\n\t@Override\n\tpublic Collection<Checkpoint> list(RunnableConfig config) {\n\t\tOptional<String> threadNameOpt = config.threadId();\n\t\tif (!threadNameOpt.isPresent()) {\n\t\t\tthrow new IllegalArgumentException(\"threadId is not 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 (list)\n\t\t\ttryLock = lock.tryLock(500, TimeUnit.MILLISECONDS);\n\t\t\tif (!tryLock) {\n\t\t\t\treturn List.of();\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 List.of();\n\t\t\t}\n","sourceCodeStart":209,"sourceCodeEnd":245,"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#L209-L245","documentation":"RedisSaver.list(CheckpointSaver.RunnableConfig) requires the config to carry a threadId, since checkpoints are keyed by thread in Redis. If Optional<String> threadId() is empty it throws this IllegalArgumentException before touching Redis.","triggerScenarios":"Calling saver.list(RunnableConfig.builder().build()) or building a RunnableConfig without .threadId(\"...\").","commonSituations":"Constructing RunnableConfig programmatically and forgetting the threadId; propagating a config from a graph run that never had an explicit thread id; refactored code where threadId assignment was dropped.","solutions":["Build the config with a threadId: RunnableConfig.builder().threadId(\"my-thread\").build().","Check config.threadId().isPresent() before calling list() and fall back to a default thread id.","Trace where the RunnableConfig originates and ensure threadId is set at graph/stream invocation time."],"exampleFix":"// before\nsaver.list(RunnableConfig.builder().build());\n// after\nsaver.list(RunnableConfig.builder().threadId(threadId).build());","handlingStrategy":"validation","validationCode":"if (config == null || config.threadId().isEmpty()) {\n    throw new IllegalStateException(\"RunnableConfig must carry a threadId before list()\");\n}","typeGuard":null,"tryCatchPattern":"try { saver.list(config); } catch (IllegalArgumentException e) { if (e.getMessage().contains(\"threadId\")) { config = RunnableConfig.builder().threadId(defaultId).build(); saver.list(config); } else throw e; }","preventionTips":["Always set threadId in RunnableConfig builders","Centralize RunnableConfig creation in one factory","Validate config before checkpoint API calls"],"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"}