alibaba/spring-ai-alibaba · warning · IllegalStateException
Thread '%s' not found or already released
Error message
Thread '%s' not found or already released
What it means
releaseThread throws IllegalStateException("Thread '%s' not found or already released") when the RELEASE_THREAD UPDATE affects 0 rows, i.e. the threadId has no active row to release. The connection is rolled back before throwing; this is a logical not-found/already-done condition, not an SQL failure.
Source
Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/h2/H2Saver.java:477
ps.setString(index, threadId);
ps.executeUpdate();
}
catch (SQLException ex) {
throw new Exception("Unable to delete retained checkpoints", ex);
}
}
@Override
protected void releaseThread(String threadId) throws Exception {
Connection conn = null;
try (Connection ignored = conn = getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(RELEASE_THREAD)) {
ps.setString(1, threadId);
int rowsAffected = ps.executeUpdate();
if (rowsAffected == 0) {
conn.rollback();
throw new IllegalStateException(format("Thread '%s' not found or already released", threadId));
}
}
conn.commit();
}
catch (SQLException ex) {
rollback(conn, threadId);
throw new Exception("Unable to release checkpoint", ex);
}
}
protected Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
private void rollback(Connection conn, String threadId) {
if (conn == null) {
return;
}View on GitHub (pinned to f82da0b50f)
Solutions
- Make release idempotent: treat this IllegalStateException as 'already released' and continue.
- Verify the threadId is the active id for that thread in this database (check the threads table).
- Guard the release call so it runs at most once per thread lifecycle.
- If the id comes from another environment/database, re-fetch it from the correct source.
Example fix
// before: shutdown hook releases twice
releaseThread(threadId); // IllegalStateException on 2nd call
// after: idempotent release
try { releaseThread(threadId); }
catch (IllegalStateException e) { log.debug("thread {} already released", threadId); } Defensive patterns
Strategy: try-catch
Validate before calling
Optional<String> active = saver.getActiveThreadId(threadName); if (active.isEmpty()) return; // nothing to release
Try / catch
try { releaseThread(threadId); }
catch (IllegalStateException e) { log.debug("thread {} already released", threadId); } // idempotent release Prevention
- Make thread release idempotent — catch and ignore this state
- Release each thread exactly once (guard with a flag or lock)
- Fetch threadIds from the same database instance used by the saver
When it happens
Trigger: Calling the thread-release API with a threadId that was never created, was already released (previous call succeeded), or whose rows were deleted from the checkpoints table.
Common situations: Double-invocation of cleanup/shutdown logic that releases the same thread twice; releasing a thread id obtained from a different database; cleanup running after retention already deleted the thread's checkpoints.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/03135079146ef0e0.
Report an issue: GitHub.