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; 018 019import static com.echothree.model.jooq.server.tables.core.EntityIds.EntityIds; 020import com.echothree.util.common.exception.PersistenceDatabaseException; 021import java.sql.SQLException; 022import java.util.concurrent.locks.ReentrantLock; 023import org.jooq.exception.DataAccessException; 024import org.jooq.impl.DSL; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028public class EntityIdGenerator { 029 030 private static final Logger log = LoggerFactory.getLogger(EntityIdGenerator.class); 031 032 static private final int defaultChunkSize = 100; 033 static private final int retryCount = 5; 034 035 private final ReentrantLock lock = new ReentrantLock(true); 036 037 private String componentVendorName; 038 private String entityTypeName; 039 private int chunkSize; 040 private long guardedValue = 0; 041 private long currentValue = 0; 042 043 private void init(String componentVendorName, String entityTypeName, int chunkSize) { 044 log.debug("EntityIdGenerator(componentVendorName = {}, entityTypeName = {}, chunkSize = {})", 045 componentVendorName, entityTypeName, chunkSize); 046 047 this.componentVendorName = componentVendorName; 048 this.entityTypeName = entityTypeName; 049 this.chunkSize = chunkSize; 050 } 051 052 /** Creates a new instance of EntityIdGenerator */ 053 public EntityIdGenerator(String componentVendorName, String entityTypeName, int chunkSize) { 054 init(componentVendorName, entityTypeName, chunkSize); 055 } 056 057 /** Creates a new instance of EntityIdGenerator */ 058 public EntityIdGenerator(String componentVendorName, String entityTypeName) { 059 init(componentVendorName, entityTypeName, defaultChunkSize); 060 } 061 062 private boolean getNewChunk() { 063 log.debug("getNewChunk(), componentVendorName = {}, entityTypeName = {}", 064 componentVendorName, entityTypeName); 065 066 var context = DslContextFactory.getInstance().getNTDslContext(); 067 068 // Keep the read and conditional write on one nontransactional connection. 069 // Do not enlist ID reservations in the caller's transaction. 070 return context.connectionResult(connection -> { 071 var dslContext = DSL.using(context.configuration().derive(connection)); 072 var currentMax = dslContext 073 .select(EntityIds.LAST_ENTITY) 074 .from(EntityIds) 075 .where(EntityIds.COMPONENT_VENDOR_NAME.eq(componentVendorName), 076 EntityIds.ENTITY_TYPE_NAME.eq(entityTypeName)) 077 .fetchOne(); 078 079 if(currentMax != null) { 080 var lastEntityId = currentMax.value1(); 081 var nextMax = lastEntityId + chunkSize; 082 var rowCount = dslContext 083 .update(EntityIds) 084 .set(EntityIds.LAST_ENTITY, nextMax) 085 .where(EntityIds.COMPONENT_VENDOR_NAME.eq(componentVendorName), 086 EntityIds.ENTITY_TYPE_NAME.eq(entityTypeName), 087 EntityIds.LAST_ENTITY.eq(lastEntityId)) 088 .execute(); 089 090 // A competing allocator may have reserved this range first. 091 if(rowCount != 1) { 092 return false; 093 } 094 095 guardedValue = nextMax; 096 currentValue = lastEntityId; 097 } else { 098 var rowCount = dslContext 099 .insertInto(EntityIds, EntityIds.COMPONENT_VENDOR_NAME, EntityIds.ENTITY_TYPE_NAME, EntityIds.LAST_ENTITY) 100 .values(componentVendorName, entityTypeName, (long)chunkSize) 101 .execute(); 102 103 if(rowCount != 1) { 104 return false; 105 } 106 107 guardedValue = chunkSize; 108 currentValue = 0; 109 } 110 111 return true; 112 }); 113 } 114 115 public long getNextEntityId() { 116 long nextEntityId; 117 118 lock.lock(); 119 try { 120 if(currentValue == guardedValue) { 121 try { 122 for(var iter = 0; iter <= retryCount; iter++) { 123 if(getNewChunk()) 124 break; 125 } 126 } catch (DataAccessException ex) { 127 var sqlException = ex.getCause(SQLException.class); 128 throw new PersistenceDatabaseException(sqlException == null ? ex : sqlException); 129 } 130 } 131 132 if(currentValue == guardedValue) { 133 throw new PersistenceDatabaseException("Could not get next increment value"); 134 } 135 136 nextEntityId = ++currentValue; 137 } finally { 138 lock.unlock(); 139 } 140 141 log.debug("getNextEntityId(), nextEntityId = {}, componentVendorName = {}, entityTypeName = {}", 142 nextEntityId, componentVendorName, entityTypeName); 143 144 return nextEntityId; 145 } 146 147}