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

  1. Make release idempotent: treat this IllegalStateException as 'already released' and continue.
  2. Verify the threadId is the active id for that thread in this database (check the threads table).
  3. Guard the release call so it runs at most once per thread lifecycle.
  4. 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

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.