001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.util.server.persistence.mutex; 018 019import com.echothree.util.server.persistence.Session; 020import java.nio.ByteBuffer; 021import java.nio.charset.StandardCharsets; 022import java.util.Objects; 023import java.util.concurrent.TimeUnit; 024import javax.enterprise.context.ApplicationScoped; 025import javax.inject.Inject; 026import org.apache.commons.codec.digest.DigestUtils; 027import org.jooq.impl.DSL; 028 029/** 030 * Postgres session advisory locks keyed by the first 64 bits of SHA-256. 031 * Hash collisions cause otherwise distinct names to share a lock. 032 */ 033@ApplicationScoped 034@DatabaseMutexFlavor(DatabaseMutexFlavor.Flavor.POSTGRES) 035public class DatabasePostgresMutex 036 implements DatabaseMutex { 037 038 private static final long POLL_INTERVAL_NANOS = TimeUnit.MILLISECONDS.toNanos(50); 039 040 @Inject 041 Session session; 042 043 private long lockKey(final String lockName) { 044 var bytes = Objects.requireNonNull(lockName, "lockName").getBytes(StandardCharsets.UTF_8); 045 046 // ByteBuffer reads in big-endian order; keep this mapping stable across callers. 047 return ByteBuffer.wrap(DigestUtils.sha256(bytes)).getLong(); 048 } 049 050 @Override 051 public boolean acquireLock(final String lockName, final int timeoutSeconds) { 052 var key = lockKey(lockName); 053 var timeoutNanos = TimeUnit.SECONDS.toNanos(timeoutSeconds); 054 var started = System.nanoTime(); 055 var acquired = false; 056 057 do { 058 if(Thread.currentThread().isInterrupted()) { 059 break; 060 } 061 062 var result = session.getDslContext() 063 .select(DSL.function("pg_try_advisory_lock", Boolean.class, DSL.val(key))) 064 .fetchOne(0, Boolean.class); 065 066 acquired = Boolean.TRUE.equals(result); 067 068 if(acquired || result == null) { 069 break; 070 } 071 072 var remaining = timeoutSeconds < 0 ? Long.MAX_VALUE : timeoutNanos - (System.nanoTime() - started); 073 if(remaining <= 0) { 074 break; 075 } 076 077 try { 078 TimeUnit.NANOSECONDS.sleep(Math.min(POLL_INTERVAL_NANOS, remaining)); 079 } catch(InterruptedException e) { 080 Thread.currentThread().interrupt(); 081 break; 082 } 083 } while(timeoutSeconds < 0 || System.nanoTime() - started < timeoutNanos); 084 085 return acquired; 086 } 087 088 @Override 089 public boolean releaseLock(final String lockName) { 090 var result = session.getDslContext() 091 .select(DSL.function("pg_advisory_unlock", Boolean.class, DSL.val(lockKey(lockName)))) 092 .fetchOne(0, Boolean.class); 093 094 return Boolean.TRUE.equals(result); 095 } 096 097}