All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.biz.sequences.Sequences Maven / Gradle / Ivy

There is a newer version: 9.6
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.biz.sequences;

import sirius.db.mixing.Entity;
import sirius.db.mixing.OMA;
import sirius.kernel.commons.Wait;
import sirius.kernel.di.std.Framework;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.HandledException;
import sirius.kernel.health.Log;

import java.sql.SQLException;

/**
 * Provides a facility to generate unique consecutive numbers.
 * 

* For each sequence name, a call to {@link #generateId(String)} will return a unique number. The initial or next * number being returned can be specified by {@link #setCounterValue(String, long, boolean)}. *

* Note that these sequences are global and not tenant aware. Therefore care must be taken to generate unique names for * sequences. A viable option is to use {@link Entity#getUniqueName()} of the entity which utilizes the generator. */ @Framework("biz.sequences") @Register(classes = Sequences.class) public class Sequences { public static final Log LOG = Log.get("sequences"); @Part private OMA oma; /** * Returns the next value in the given sequence. *

* Note that this method doesn't use locks or transactions. It rather utilizes optimistic locking, which scales * extremely well. However, the algorithm used is not intended for extreme parallel usage. In such scenarios, * it will not content but rather give up after some tries and report an appropriate exception. * * @param sequence the name of the sequence which is counted up. * @return the next value (which has not yet been returned). If the sequence is unknown, 1 is returned. * @throws HandledException If the system was unable to generate a unique sequence number. This might happen in * extreme load conditions, as internally an optimistic locking algorithm is employed. */ public long generateId(String sequence) { try { int retries = 25; while (retries-- > 0) { Long id = tryGenerateId(sequence); if (id != null) { return id; } // Block a short random amount of time to resolve conflicts with other waiting threads Wait.randomMillis(50, 100); } throw Exceptions.handle() .to(LOG) .withSystemErrorMessage( "Failed to generate an unique sequence number for %s. Giving up after 25 retries.", sequence) .handle(); } catch (SQLException e) { throw Exceptions.handle() .to(LOG) .error(e) .withSystemErrorMessage( "Failed to generate an unique number for %s due to a database error: %s", sequence) .handle(); } } private Long tryGenerateId(String sequence) throws SQLException { // Select the current value which will be returned if all goes well.... SequenceCounter result = oma.select(SequenceCounter.class).eq(SequenceCounter.NAME, sequence).queryFirst(); if (result == null) { return createSequence(sequence); } int numRowsChanged = oma.getDatabase() .createQuery("UPDATE sequencecounter" + " SET nextValue = nextValue + 1" + " WHERE name = ${name} " + " AND nextValue = ${value}") .set("name", sequence) .set("value", result.getNextValue()) .executeUpdate(); if (numRowsChanged == 1) { // Nobody else changed the counter, so we can savely return the determined value... return result.getNextValue(); } return null; } private Long createSequence(String sequence) { SequenceCounter result; try { // Try to create a new record, as no counter is yet present... result = new SequenceCounter(); result.setName(sequence); result.setNextValue(2); oma.update(result); return 1L; } catch (HandledException e) { // This only happens if another thread / server inserted the entity already... Exceptions.ignore(e); return null; } } /** * Sets the initial or next value of the given sequence. *

* Unless force is set to true, the value has to be higher than the current counter value to * prevent non unique numbers from being generated. * * @param sequence the sequence to update * @param nextValue the next value that will be returned when calling {@link #generateId(String)} for this * sequence. * @param force if true, no sanity checks are performed and the sequence can be reset to ANY * value. This is rather dangerous, as it might lead to the generation of duplicate ids. If * set to false, the given nextValue has to be higher than the current sequence * value. */ public void setCounterValue(String sequence, long nextValue, boolean force) { try { // Select the current value which will be returned if all goes well.... if (oma.select(SequenceCounter.class).eq(SequenceCounter.NAME, sequence).exists()) { updateCounterValue(sequence, nextValue, force); } else { createSequenceWithValue(sequence, nextValue); } } catch (SQLException e) { throw Exceptions.handle() .to(LOG) .error(e) .withSystemErrorMessage( "Failed to specify the next value for sequence %s due to a database error: %s", sequence) .handle(); } } private void createSequenceWithValue(String sequence, long nextValue) { try { // Try to create a new record, as no counter is yet present... SequenceCounter counter = new SequenceCounter(); counter.setName(sequence); counter.setNextValue(nextValue); oma.update(counter); } catch (HandledException e) { throw Exceptions.handle() .to(LOG) .error(e) .withSystemErrorMessage("Failed to specify the next value for sequence %s - %s (%s)", sequence) .handle(); } } private void updateCounterValue(String sequence, long nextValue, boolean force) throws SQLException { String sql = "UPDATE sequencecounter SET nextValue = ${value} WHERE name = ${name}"; if (!force) { sql += " AND nextValue <= ${value}"; } int updatedRows = oma.getDatabase().createQuery(sql).set("name", sequence).set("value", nextValue).executeUpdate(); if (updatedRows != 1) { throw Exceptions.handle() .to(LOG) .withSystemErrorMessage("Failed to specify the next value for sequence %s", sequence) .handle(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy