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}