Skip to content

Commit

Permalink
Add seqno module
Browse files Browse the repository at this point in the history
  • Loading branch information
ar committed Jan 9, 2019
1 parent c59e3e9 commit f7a9d9d
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/src/asciidoc/book.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ include::module_fsdmsgX.adoc[]
include::entities/sysconfig.adoc[]
include::entities/syslog.adoc[]
include::entities/eeuser.adoc[]
include::entities/seqno.adoc[]
= Appendices
Expand Down
66 changes: 66 additions & 0 deletions doc/src/asciidoc/entities/seqno.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[[seqno]]
== SeqNo

[plantuml, sysconfig, svg]
----
@startuml
class SeqNo {
String id;
long value;
long lockedBy;
long lockUntil;
}
@enduml
----

The `SeqNo` entity footnote:['seqno' table] is a general purpose entity
used by jPOS-EE to store application specific sequencers.

Typical use case would be terminal-level STANS, Voucher IDs and the like.

The `SeqNoManager` supports two operating modes:

- Synchronous
- Asynchronous

In Synchronous mode, the transaction life-cycle is handled by the caller, e.g.:

[source,java]
-------------
private long next(String id) {
try (DB db = new DB()) {
SeqNoManager mgr = new SeqNoManager(db);
db.open();
db.beginTransaction();
long l = mgr.next(id, 999999L);
db.commit();
return l;
}
}
-------------

[NOTE]
======
In 'sync' mode, a second thread trying to obtain a sequence number will block until the
former is committed.
======

In Asynchronous mode, the JDBC connection is released and an explicit call to `SeqNoManager.release`
has to be issued, e.g.:

[source,java]
-------------
SeqNoManager mgr = new SeqNoManager(new DB());
long l = mgr.next("sync", id, 60000L, 60000L, 999999L);
// ... do something, e.g.: query remote host
mgr.release("sync", id);
return l;
-------------

Interesting thing about async mode, is that while one calls a remote host (i.e. using `QueryHost`),
the JDBC connection gets released back to its pool.


26 changes: 26 additions & 0 deletions modules/seqno/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
description = 'jPOS-EE :: SeqNo'

dependencies {
compile project(':modules:dbsupport')
testCompile project(':modules:db-h2')
// compile project(':modules:db-mysql')
}


ext {
testRuntimeDir = "$buildDir/runtime" as File
}

task jposeeSetup(dependsOn: 'classes', type: JavaExec) {
classpath = sourceSets.test.runtimeClasspath
main = 'org.jpos.q2.install.Install'
args = ["--quiet", "--force", "--outputDir=" + testRuntimeDir]
}

test {
dependsOn 'jposeeSetup'
scanForTestClasses true
workingDir testRuntimeDir
}

apply from: "${rootProject.projectDir}/jpos-app.gradle"
Empty file.
129 changes: 129 additions & 0 deletions modules/seqno/src/main/java/org/jpos/ee/SeqNo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2019 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.jpos.ee;

import java.io.Serializable;
import java.util.Objects;

import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;

/**
* SeqNo can be used to manage application level sequencers and both synchronous as well as asynchronous locking.
*
* In synchronous mode, a lock is placed on <code>SeqNo.id</code>.
* In asynchronous mode, a unique <code>lockedBy</code> identifier is used, and the entry is considered locked
* until a given timeout, or gets released by calling <code>SeqNoManager.release</code>.
*
* While this entity support both modes (sync/async), applications shouldn't mix them.
*
* @see org.jpos.ee.SeqNoManager
* @since 2.2.6
*/

@Entity
@Table(name = "seqno")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@SuppressWarnings("unused")
public class SeqNo extends Cloneable implements Serializable {
private static final long serialVersionUID = -1475470843801587648L;
@Id
@Column(length=128)
private String id;

private long value;
private long lockedBy;
private long lockUntil;

public SeqNo(String id) {
this.id = id;
}

public SeqNo() {
super();
}

public String getId() {
return this.id;
}

public void setId(String id) {
this.id = id;
}

public long getValue() {
return this.value;
}

public void setValue(long value) {
this.value = value;
}

/**
* Returns next sequence number.
*
* @param wrapAt greater than wraps back to 1
* @return next sequence number
*/
public long next (long wrapAt) {
synchronized (this) {
if (++value > wrapAt)
value = 1;
}
return value;
}

public long getLockedBy() {
return lockedBy;
}

public void setLockedBy(long lockedBy) {
this.lockedBy = lockedBy;
}

public long getLockUntil() {
return lockUntil;
}

public void setLockUntil(long lockUntil) {
this.lockUntil = lockUntil;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SeqNo seqNo = (SeqNo) o;
return value == seqNo.value && Objects.equals(id, seqNo.id);
}

@Override
public int hashCode() {
return Objects.hash(id, value);
}

@Override
public String toString() {
return "SeqNo{" +
"id='" + id + '\'' +
", value=" + value +
'}';
}
}
137 changes: 137 additions & 0 deletions modules/seqno/src/main/java/org/jpos/ee/SeqNoManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2019 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.jpos.ee;

import org.hibernate.LockMode;
import org.jpos.iso.ISOUtil;
import javax.persistence.LockTimeoutException;

/**
* SeqNoManager can be used to manage application level sequencers and both synchronous as well as asynchronous locking.
*
* This Manager can operate in synchronous or asynchronous way.
* In sync mode, the caller needs to provide the transaction life-cycle
* (<code>open, beginTransaction, commit, close</code>).
*
* In async mode the application has to provide a pristine (not opened) DB object
* and the manager takes care of the transaction lifecycle.
*
* @see org.jpos.ee.SeqNoManager
* @since 2.2.6
*/
public class SeqNoManager {
private DB db;

public SeqNoManager(DB db) {
this.db = db;
}


/**
* Synchronous 'next'
* @param id sequencer id
* @param wrapAt wrap at value
* @return next sequencer value
*/
public long next (String id, long wrapAt) {
return getOrCreate(id).next(wrapAt);
}

/**
* Synchronous 'reset'
* @param id sequencer id
* @param value reset value
*/
public void reset (String id, long value) {
getOrCreate(id).setValue(value);
}

/**
* Asynchronous 'next'
*
* @param id sequencer ID
* @param lockedBy unique client identifier (any long, has to be system-wide unique)
* @param lockTimeout once lock is obtained, keep it fo 'lockTimeout' millis (if not 'released' earlier)
* @param timeout time (in millis) to wait for this lock
* @param wrapAt wrap at value
* @return next sequencer value
* @throws LockTimeoutException if lock can't be obtained after 'timeout' has elapsed
*/
public long next (String id, long lockedBy, long lockTimeout, long timeout, long wrapAt) {
long until = System.currentTimeMillis() + timeout;
if (db.session != null && db.session.isOpen())
throw new IllegalStateException("DB should not be open");
while (System.currentTimeMillis() < until) {
try (DB db1 = db) {
db1.open();
db1.beginTransaction();
SeqNo seq = getOrCreate(id);
long now = System.currentTimeMillis();
if (seq.getLockedBy() == 0 || seq.getLockUntil() < now) {
seq.setLockedBy(lockedBy);
seq.setLockUntil(now + lockTimeout);
long stan = seq.next(wrapAt);
db1.commit();
return stan;
}
db1.commit();
ISOUtil.sleep(500L);
}
}
throw new LockTimeoutException("Unable to lock " + id + " in less than " + timeout + " millis");
}

/**
* Release an async lock
* @param id lock ID
* @param lockedBy unique client identifier
*/
public void release (String id, long lockedBy) {
if (db.session != null && db.session.isOpen())
throw new IllegalStateException("DB should not be open");
try (DB db1 = db) {
db1.open();
db1.beginTransaction();
SeqNo seq = getOrCreate(id);
if (seq.getLockedBy() == lockedBy) {
seq.setLockedBy(0L);
db1.commit();
}
}
}

private SeqNo getOrCreate(String id) {
SeqNo seq = db.session().get(SeqNo.class, id, LockMode.PESSIMISTIC_WRITE);
if (seq == null) {
create (id);
seq = db.session().get(SeqNo.class, id, LockMode.PESSIMISTIC_WRITE);
}
return seq;
}

private void create (String id) {
try (DB db = new DB()) {
db.open();
db.beginTransaction();
SeqNo seq = new SeqNo(id);
db.session().save(seq);
db.commit();
} catch (Exception ignored) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<module name="seqno">
<mappings>
<mapping class="org.jpos.ee.SeqNo"/>
</mappings>
</module>

Loading

0 comments on commit f7a9d9d

Please sign in to comment.