DmoVersioning.java
/*
** Module : DmoVersioning.java
** Abstract : Manages the versions of database records to enable stale record detection
**
** Copyright (c) 2020-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20200903 Created initial version.
** 002 CA 20250303 Added 'increment', used to invalidate cross-session cached records, when they are modified
** via SQL executed against the database.
*/
/*
** 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 may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist.orm;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import com.goldencode.p2j.persist.*;
/**
* Provides version tracking services for data model objects (DMOs), to allow a user context to verify that
* a record it has in its session cache represents the most recent version of that record in the database.
* One instance of this class is created for each persistent database. Temp-table records are not versioned,
* since they are local to a particular user context.
* <p>
* <strong>Note:</strong> This class keeps track of changes made within the FWD application server only.
* No version information is stored within the database, as that would involve an intrusive schema change.
* So, if a record is modified externally, this tracker will be unaware of the change.
*/
public class DmoVersioning
{
/** DMO version value which indicates the DMO is not currently being tracked for versioning */
static final int UNVERSIONED = -1;
/** Index of current version in atomic integer array */
static final int VERSION = 0;
/** Index of reference count in atomic integer array */
private static final int REFCOUNT = 1;
/** Map of record identifier to current version information for tracked records */
private ConcurrentMap<RecordIdentifier<String>, AtomicIntegerArray> versions = new ConcurrentHashMap<>();
/**
* Default constructor.
*/
public DmoVersioning()
{
}
/**
* Increment the version of the record with the specified identifier.
*
* @param ident
* Record identifier unique to the associated database.
*/
public void increment(RecordIdentifier<String> ident)
{
AtomicIntegerArray v = versions.get(ident);
if (v != null)
{
int version = UNVERSIONED;
do
{
version = v.incrementAndGet(VERSION);
}
while (version == UNVERSIONED);
}
}
/**
* Increment the version of all the records from the specified table.
*
* @param dmoImplName
* The DMO implementation name.
*/
public void increment(String dmoImplName)
{
versions.forEach((recordIdent, v) ->
{
if (recordIdent.getTable().equals(dmoImplName))
{
int version = UNVERSIONED;
do
{
version = v.incrementAndGet(VERSION);
}
while (version == UNVERSIONED);
}
});
}
/**
* Begin tracking the record represented by the given identifier. If it already is being tracked, its
* reference count is incremented.
* <p>
* Since the data structure uses reference counting to determine whether any user contexts are interested
* in this record, it is essential that this method and {@link #deregister(RecordIdentifier)} are called
* in matching pairs for any record. Otherwise the reference count could get out of balance, creating a
* memory leak, or releasing an entry too soon.
*
* @param ident
* Record identifier unique to the associated database.
*
* @return The atomic integer array which can be used to get and update version and reference count
* information for the given key's corresponding record.
*/
public AtomicIntegerArray register(RecordIdentifier<String> ident)
{
// mutable flag accessible to mapping function to ensure atomic integer array values are only
// modified once, if mapping function is executed multiple times
boolean[] modified = new boolean[] { false };
AtomicIntegerArray info = versions.compute(ident, (k, v) ->
{
if (v == null)
{
v = new AtomicIntegerArray(2);
}
if (!modified[0])
{
// update flag to ensure atomic integer array is only modified once per track request
modified[0] = true;
// value of atomic integer at element 1 in the array is the reference count of sessions who
// are interested in the version of this DMO
v.incrementAndGet(REFCOUNT);
}
return v;
});
return info;
}
/**
* Stop tracking the
* <p>
* Since the data structure uses reference counting to determine whether any user contexts are interested
* in this record, it is essential that this method and {@link #register(RecordIdentifier)} are called
* in matching pairs for any record. Otherwise the reference count could get out of balance, creating a
* memory leak, or releasing an entry too soon.
*
* @param ident
* Record identifier unique to the associated database.
*/
public void deregister(RecordIdentifier<String> ident)
{
// mutable flag accessible to mapping function to ensure atomic integer array values are only
// modified once, if mapping function is executed multiple times
boolean[] modified = new boolean[] { false };
versions.computeIfPresent(ident, (k, v) ->
{
if (!modified[0])
{
// update flag to ensure atomic integer array is only modified once per track request
modified[0] = true;
// value of atomic integer at element 1 in the array is the reference count of sessions who
// are interested in the version of this DMO
if (v.decrementAndGet(REFCOUNT) == 0)
{
// no one is interested in this record's version any longer; remove the entry
v = null;
}
}
return v;
});
}
}