alibaba/spring-ai-alibaba · error · IllegalStateException

Thread '%s' not found or already released

Error message

Thread '%s' not found or already released

What it means

PostgresSaver.releaseThread executes an UPDATE that clears the lock owner on the checkpoints thread row. If executeUpdate() reports 0 affected rows, the transaction is rolled back and this IllegalStateException is thrown, meaning no thread row with that thread_id existed (or its lock was already released by someone else). It is an optimistic guard against releasing a thread that is not currently held.

Source

Thrown at spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/checkpoint/savers/postgresql/PostgresSaver.java:585

			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);
			log.trace("Executing release Thread:\n---\n{}---", RELEASE_THREAD);
			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();
			log.debug("Thread {} released successfully.", threadId);
		}
		catch (SQLException ex) {
			log.error("Error releasing thread {}", threadId, ex);
			rollback(conn, threadId);
			throw new Exception("Unable to release checkpoint", ex);
		}
	}

	/**
	 * Datasource connection
	 *
	 * @return Datasource connection
	 * @throws SQLException exception
	 */

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Verify the threadId exists and is currently locked by querying the checkpoints thread table before calling releaseThread.
  2. Treat 0-rows-affected as an idempotent no-op if your cleanup logic allows double-release; catch IllegalStateException and log instead of failing.
  3. Ensure all instances use the same lock/owner convention; check for another node releasing the thread concurrently.
  4. Confirm the threadId string matches exactly (case, whitespace) the one used when the thread was created.

Example fix

// before
saver.releaseThread(conn, threadId);
// after
try {
    saver.releaseThread(conn, threadId);
} catch (IllegalStateException e) {
    log.warn("Thread {} was not locked; nothing to release", threadId);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// caller-side precheck
ResultSet rs = conn.prepareStatement("SELECT 1 FROM thread_locks WHERE thread_id = '" + threadId.replace("'", "") + "'").executeQuery();
boolean exists = rs.next();

Try / catch

try { saver.releaseThread(conn, threadId); } catch (IllegalStateException e) { log.info("Thread {} already released; ignoring", threadId); }

Prevention

When it happens

Trigger: Calling PostgresSaver.releaseThread(conn, threadId) with a threadId that was never created via a checkpoint release/lock insert, or calling releaseThread twice on the same thread, or after another process/instance already released the lock row.

Common situations: Race between two app instances sharing the same Postgres checkpoint DB; cleanup code that releases threads on shutdown which were never locked; stale thread IDs from a wiped or migrated database; typo'd threadId compared to the one used when releasing/creating checkpoints.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/5004528ebf31fe4d. Report an issue: GitHub.