hibernate/hibernate-orm · error · UnsupportedOperationException

Spanner does not support skip locked.

Error message

Spanner does not support skip locked.

What it means

This is the SKIP_LOCKED branch of SpannerPostgreSQLDialect.validateSpannerLockTimeout(): millis == Timeouts.SKIP_LOCKED_MILLI throws UnsupportedOperationException('Spanner does not support skip locked.'). It is invoked from getLockingClauseStrategy() whenever a query with lock options is translated, so a skip-locked request fails before SQL generation. Spanner's transaction model aborts conflicting accessors rather than skipping rows, hence the hard rejection.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/dialect/SpannerPostgreSQLDialect.java:535

		return getForUpdateSkipLockedString();
	}

	@Override
	public LockingClauseStrategy getLockingClauseStrategy(
			QuerySpec querySpec, LockOptions lockOptions) {
		if ( lockOptions == null ) {
			return NON_CLAUSE_STRATEGY;
		}
		validateSpannerLockTimeout( lockOptions.getTimeOut() );
		return super.getLockingClauseStrategy( querySpec, lockOptions );
	}

	private static void validateSpannerLockTimeout(int millis) {
		if ( Timeouts.isRealTimeout( millis ) ) {
			throw new UnsupportedOperationException( "Spanner does not support lock timeout." );
		}
		if ( millis == Timeouts.SKIP_LOCKED_MILLI ) {
			throw new UnsupportedOperationException( "Spanner does not support skip locked." );
		}
		if ( millis == Timeouts.NO_WAIT_MILLI ) {
			throw new UnsupportedOperationException( "Spanner does not support no wait." );
		}
	}

	@Override
	public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
		super.contributeTypes( typeContributions, serviceRegistry );

		final var configurationService = serviceRegistry.requireService( ConfigurationService.class );

		this.useIntegerForPrimaryKey = configurationService.getSetting(
				USE_INTEGER_FOR_PRIMARY_KEY,
				StandardConverters.BOOLEAN,
				false
		);

View on GitHub (pinned to fad1729dce)

Solutions

  1. Remove the -2 timeout hint and port the claim logic to a lease-based conditional UPDATE, the supported pattern on Spanner.
  2. Use optimistic locking + retry instead of skip-locked semantics.
  3. Catch UnsupportedOperationException in dialect-agnostic code paths and fall back to plain FOR UPDATE.
  4. Profile-separate JPA lock properties so Spanner never receives SKIP_LOCKED.

Example fix

// before
Query q = session.createQuery("from Task t where t.status = 'OPEN'", Task.class);
q.getLockOptions().setTimeOut(LockOptions.SKIP_LOCKED); // -2

// after
Query q = session.createQuery("from Task t where t.status = 'OPEN' and t.leaseUntil < :now", Task.class);
Defensive patterns

Strategy: validation

Validate before calling

if (lockOptions.getTimeOut() == Timeouts.SKIP_LOCKED_MILLI && dialect instanceof SpannerPostgreSQLDialect) {
  lockOptions.setTimeOut(LockOptions.NO_TIMEOUT);
}

Try / catch

try { return query.getResultList(); }
catch (UnsupportedOperationException e) {
  if (e.getMessage().contains("skip locked")) { /* lease-claim fallback */ }
  throw e;
}

Prevention

When it happens

Trigger: On the Spanner PostgreSQL dialect: lock timeout -2 (LockOptions.SKIP_LOCKED or jakarta.persistence.lock.timeout=-2) attached to a query/finder with pessimistic locking; getLockingClauseStrategy reads lockOptions.getTimeOut(), hits the sentinel and throws.

Common situations: PG-native queue processors (SKIP LOCKED worklists) pointed at Spanner's PG interface; Spring properties copied between environments; integration test suites asserting lock behaviors that pass on PostgreSQL CI and fail on Spanner stages.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/f15bb4753af32c1b. Report an issue: GitHub.