2 |
2 |
** Module : LockTableUpdater.java
|
3 |
3 |
** Abstract : Manages metadata lock records for a particular, primary database
|
4 |
4 |
**
|
5 |
|
** Copyright (c) 2013-2020, Golden Code Development Corporation.
|
|
5 |
** Copyright (c) 2013-2022, Golden Code Development Corporation.
|
6 |
6 |
**
|
7 |
7 |
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
|
8 |
8 |
** 001 ECF 20131101 Created initial version.
|
... | ... | |
23 |
23 |
** 014 ECF 20200906 New ORM implementation.
|
24 |
24 |
** 015 CA 20200924 Replaced Method.invoke with ReflectASM.
|
25 |
25 |
** OM 20201012 Force use locally cached meta information instead of map lookup.
|
|
26 |
** IAS 20220324 Performance optimization in the UserTableStatUpdater style.
|
26 |
27 |
*/
|
27 |
28 |
|
28 |
29 |
/*
|
... | ... | |
82 |
83 |
|
83 |
84 |
import java.lang.reflect.*;
|
84 |
85 |
import java.util.*;
|
|
86 |
import java.util.concurrent.*;
|
|
87 |
import java.util.concurrent.atomic.*;
|
|
88 |
import java.util.concurrent.locks.*;
|
85 |
89 |
import java.util.logging.*;
|
86 |
90 |
|
|
91 |
import javax.management.loading.*;
|
|
92 |
|
|
93 |
import org.apache.jasper.tagplugins.jstl.core.*;
|
|
94 |
|
|
95 |
import com.goldencode.p2j.jmx.*;
|
87 |
96 |
import com.goldencode.p2j.persist.*;
|
88 |
97 |
import com.goldencode.p2j.persist.lock.*;
|
89 |
98 |
import com.goldencode.p2j.persist.orm.*;
|
... | ... | |
168 |
177 |
|
169 |
178 |
/**
|
170 |
179 |
* Constructor.
|
171 |
|
*
|
|
180 |
* @param primary
|
|
181 |
* Primary database
|
172 |
182 |
* @param metaDatabase
|
173 |
183 |
* Metadata database.
|
174 |
184 |
*/
|
175 |
|
public LockTableUpdater(Database metaDatabase)
|
176 |
|
{
|
177 |
|
this.worker = new UpdateWorker(PersistenceFactory.getInstance(metaDatabase));
|
178 |
|
}
|
179 |
|
|
180 |
|
/**
|
181 |
|
* This method is called within a user context to force any pending lock events to be
|
182 |
|
* processed. It guarantees that any events that are pending in the queue at the time this
|
183 |
|
* method is invoked will have been processed by the time this method returns. However,
|
184 |
|
* other events may be added by other user contexts in the meantime, and these are not
|
185 |
|
* guaranteed to have been processed.
|
186 |
|
*/
|
187 |
|
public void flushQueue()
|
188 |
|
{
|
189 |
|
worker.forceFlush();
|
190 |
|
}
|
191 |
|
|
192 |
|
/**
|
193 |
|
* Stop processing record lock events for the associated, primary database. This will terminate
|
194 |
|
* the update worker and its processing thread.
|
195 |
|
*/
|
196 |
|
public void terminate()
|
197 |
|
{
|
198 |
|
worker.terminate();
|
199 |
|
}
|
200 |
|
|
|
185 |
public LockTableUpdater(Database primary, Database metaDatabase)
|
|
186 |
{
|
|
187 |
this.worker = new UpdateWorker(
|
|
188 |
PersistenceFactory.getInstance(metaDatabase),
|
|
189 |
primary);
|
|
190 |
}
|
|
191 |
|
|
192 |
/**
|
|
193 |
* Persist locks in the meta database
|
|
194 |
*/
|
|
195 |
public void persist()
|
|
196 |
{
|
|
197 |
worker.persist();
|
|
198 |
}
|
201 |
199 |
/**
|
202 |
200 |
* Method which is called by a {@link LockManager} in response to a lock event. Does some
|
203 |
201 |
* preliminary processing and sanity checking, then queues the event for further processing on
|
... | ... | |
244 |
242 |
* thread processes the events.
|
245 |
243 |
*/
|
246 |
244 |
private static class UpdateWorker
|
247 |
|
implements Runnable
|
|
245 |
implements LockTableUpdaterStatMBean
|
248 |
246 |
{
|
249 |
247 |
/** Metadata lock table name */
|
250 |
248 |
private static final String LOCK_TABLE = "meta_lock";
|
251 |
249 |
|
252 |
|
/** SQL query to find a lock record by lock recid and user */
|
|
250 |
/** SQL query to find a lock record by PK */
|
253 |
251 |
private static final String SQL_LOCK =
|
254 |
|
"select m." + Session.PK + " from " + LOCK_TABLE + " m " +
|
255 |
|
"where m.lock_recid = ?1 and m.lock_usr = ?2";
|
256 |
|
|
257 |
|
/** SQL query to find a file number by converted table name (used by lock manager) */
|
258 |
|
private static final String SQL_FILENUM =
|
259 |
|
"select m.file_number from meta_file m where m.user_misc = ?1";
|
|
252 |
"select m." + Session.PK + " from " + LOCK_TABLE + " m " +
|
|
253 |
"where m." + Session.PK + " = ?1";
|
260 |
254 |
|
261 |
255 |
/** The offset between a table and its surrogate chain id. */
|
262 |
256 |
private static final int CHAIN_OFFSET = 1_000_000;
|
... | ... | |
304 |
298 |
dmoClass = implementingClass;
|
305 |
299 |
}
|
306 |
300 |
|
307 |
|
/** Event queue to which many threads can write and from which only one thread reads */
|
308 |
|
private final List<RecordLockEvent> eventQueue = new LinkedList<>();
|
309 |
|
|
|
301 |
/** Number of processed lock events. */
|
|
302 |
private static final AtomicLong lockEvents = new AtomicLong(0);
|
|
303 |
|
|
304 |
/** Number of records in VST. */
|
|
305 |
private static final AtomicLong persistedLocks = new AtomicLong(0);
|
|
306 |
|
|
307 |
/** Number of VST records deleted not in batch. */
|
|
308 |
private static final AtomicLong deletesFromVST = new AtomicLong(0);
|
|
309 |
|
|
310 |
/** Number of 'persist' calls */
|
|
311 |
private static final AtomicLong persistCalls = new AtomicLong(0);
|
|
312 |
|
|
313 |
/** Executor for operations with VST */
|
|
314 |
private final Executor executor = Executors.newSingleThreadExecutor(
|
|
315 |
new ThreadFactory()
|
|
316 |
{
|
|
317 |
|
|
318 |
@Override
|
|
319 |
public Thread newThread(Runnable r)
|
|
320 |
{
|
|
321 |
Thread workThread = new AssociatedThread(r);
|
|
322 |
workThread.setDaemon(true);
|
|
323 |
workThread.setName("Lock metadata update thread");
|
|
324 |
return workThread;
|
|
325 |
}
|
|
326 |
}
|
|
327 |
);
|
|
328 |
/** Locks' map */
|
|
329 |
private final ConcurrentMap<LockRecId, MinimalLockImpl> locks = new ConcurrentSkipListMap<>();
|
|
330 |
|
|
331 |
/** Map version */
|
|
332 |
private final AtomicLong mapVersion = new AtomicLong();
|
|
333 |
|
|
334 |
/** Meta table vesrion */
|
|
335 |
private final AtomicLong databaseVersion = new AtomicLong(0);
|
|
336 |
|
|
337 |
/** Locks' map guard */
|
|
338 |
private final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();
|
|
339 |
|
|
340 |
/** database name */
|
|
341 |
private final Database primary;
|
|
342 |
|
310 |
343 |
/** Persistence helper object for metadata database */
|
311 |
344 |
private final Persistence persistence;
|
312 |
345 |
|
... | ... | |
316 |
349 |
/** Invocation handler which maps proxy method calls to underlying DMO methods */
|
317 |
350 |
private final Handler handler = new Handler();
|
318 |
351 |
|
319 |
|
/** Update worker thread */
|
320 |
|
private final AssociatedThread workThread;
|
321 |
|
|
322 |
|
/** Cache of converted table names to file numbers */
|
323 |
|
private final Map<String, Integer> fileNums = new HashMap<>();
|
324 |
|
|
325 |
|
/** Termination flag */
|
326 |
|
private boolean done = false;
|
327 |
|
|
328 |
|
/** Lock needed to read from queue; must be obtained first if write lock is needed also */
|
329 |
|
private final Object readLock = new Object();
|
330 |
|
|
331 |
|
/** Lock needed to write to queue; must be obtained second if read lock is needed also */
|
332 |
|
private final Object writeLock = new Object();
|
333 |
|
|
334 |
|
/** Unique and sequential ID of the last event added to the queue */
|
335 |
|
private long lastAddedID = -1L;
|
336 |
|
|
337 |
|
/** Unique and sequential ID of the last event read from the queue and processed */
|
338 |
|
private long lastProcessedID = -1L;
|
339 |
|
|
340 |
352 |
/** Next available primary key for a new lock metadata record */
|
341 |
|
private long nextPrimaryKey = 1L;
|
|
353 |
private AtomicLong nextPrimaryKey = new AtomicLong(1L);
|
342 |
354 |
|
343 |
355 |
/** Dummy empty class to be used as the super class for proxy. */
|
344 |
356 |
public static class Empty {}
|
... | ... | |
349 |
361 |
* @param persistence
|
350 |
362 |
* Persistence helper object for metadata database.
|
351 |
363 |
*/
|
352 |
|
UpdateWorker(Persistence persistence)
|
|
364 |
UpdateWorker(Persistence persistence, Database primary)
|
353 |
365 |
{
|
354 |
366 |
super();
|
355 |
367 |
|
356 |
368 |
this.persistence = persistence;
|
|
369 |
this.primary = primary;
|
357 |
370 |
this.proxy = ProxyFactory.getProxy(Empty.class, new Class<?>[] { MinimalLock.class }, handler);
|
|
371 |
FwdJMX.register(new LockTableUpdaterStat(this), primary.getName() + ".locks");
|
|
372 |
executor.execute(() -> {}); // start executor
|
|
373 |
}
|
|
374 |
|
|
375 |
/**
|
|
376 |
* Submit persisting locks task to the meta database and wait for end
|
|
377 |
*/
|
|
378 |
public void persist()
|
|
379 |
{
|
|
380 |
CountDownLatch latch = new CountDownLatch(1);
|
|
381 |
executor.execute(() -> {
|
|
382 |
try
|
|
383 |
{
|
|
384 |
doPersist();
|
|
385 |
latch.countDown();
|
|
386 |
}
|
|
387 |
catch(Throwable t)
|
|
388 |
{
|
|
389 |
log.log(Level.WARNING, "Failed to persist lock table data", t);
|
|
390 |
}
|
|
391 |
});
|
|
392 |
try
|
|
393 |
{
|
|
394 |
latch.await();
|
|
395 |
}
|
|
396 |
catch (InterruptedException ignore)
|
|
397 |
{
|
|
398 |
}
|
|
399 |
}
|
|
400 |
|
|
401 |
/**
|
|
402 |
* Persist locks to the meta database
|
|
403 |
*/
|
|
404 |
public void doPersist()
|
|
405 |
{
|
|
406 |
persistCalls.incrementAndGet();
|
|
407 |
guard.writeLock().lock();
|
|
408 |
try
|
|
409 |
{
|
|
410 |
if (databaseVersion.get() >= mapVersion.get())
|
|
411 |
{
|
|
412 |
return;
|
|
413 |
}
|
|
414 |
for(Iterator<Map.Entry<LockRecId, MinimalLockImpl>> it = locks.entrySet().iterator();
|
|
415 |
it.hasNext(); )
|
|
416 |
{
|
|
417 |
Map.Entry<LockRecId, MinimalLockImpl> e = it.next();
|
|
418 |
e.getValue().persist();
|
|
419 |
if (e.getValue().deleted)
|
|
420 |
{
|
|
421 |
it.remove();
|
|
422 |
}
|
|
423 |
}
|
|
424 |
databaseVersion.set(mapVersion.get());
|
|
425 |
}
|
|
426 |
finally
|
|
427 |
{
|
|
428 |
guard.writeLock().unlock();
|
|
429 |
}
|
358 |
430 |
|
359 |
|
workThread = new AssociatedThread(this);
|
360 |
|
workThread.setDaemon(true);
|
361 |
|
workThread.setName("Lock metadata update thread");
|
362 |
|
workThread.start();
|
363 |
|
}
|
364 |
|
|
365 |
|
/**
|
366 |
|
* This method executes in the worker thread, reading record lock events from the queue when
|
367 |
|
* they become available, and updating the metadata database accordingly.
|
368 |
|
*/
|
369 |
|
@Override
|
370 |
|
public void run()
|
371 |
|
{
|
372 |
|
while (true)
|
373 |
|
{
|
374 |
|
// check if we should terminate event processing
|
375 |
|
synchronized (writeLock)
|
376 |
|
{
|
377 |
|
if (done)
|
378 |
|
{
|
379 |
|
break;
|
380 |
|
}
|
381 |
|
}
|
382 |
|
|
383 |
|
// process any pending events
|
384 |
|
processPendingEvents();
|
385 |
|
|
386 |
|
// wait for new events to be added to the queue if it is still empty
|
387 |
|
synchronized (writeLock)
|
388 |
|
{
|
389 |
|
while (!done && eventQueue.isEmpty())
|
390 |
|
{
|
391 |
|
try
|
392 |
|
{
|
393 |
|
writeLock.wait();
|
394 |
|
}
|
395 |
|
catch (InterruptedException exc)
|
396 |
|
{
|
397 |
|
// may be spurious, ignore; if interrupted due to termination, we will
|
398 |
|
// detect this in the termination check above
|
399 |
|
}
|
400 |
|
}
|
401 |
|
}
|
402 |
|
}
|
403 |
|
}
|
404 |
|
|
|
431 |
}
|
405 |
432 |
/**
|
406 |
433 |
* Add a record lock event to the queue, locking out other write and read operations until
|
407 |
434 |
* the queue is updated. Notify a the worker thread when the new event has been added.
|
... | ... | |
411 |
438 |
*/
|
412 |
439 |
void addEvent(RecordLockEvent event)
|
413 |
440 |
{
|
414 |
|
synchronized (writeLock)
|
415 |
|
{
|
416 |
|
eventQueue.add(event);
|
417 |
|
// DBG: System.out.println("-> " + event);
|
418 |
|
lastAddedID = event.getEventID();
|
419 |
|
writeLock.notify();
|
420 |
|
}
|
421 |
|
}
|
422 |
|
|
423 |
|
/**
|
424 |
|
* This method is called within a user context to force any pending lock events to be
|
425 |
|
* processed. It guarantees that any events that are pending in the queue at the time this
|
426 |
|
* method is invoked will have been processed by the time this method returns. However,
|
427 |
|
* other events may be added by other user contexts in the meantime, and these are not
|
428 |
|
* guaranteed to have been processed.
|
429 |
|
*/
|
430 |
|
void forceFlush()
|
431 |
|
{
|
432 |
|
long markerID = -1L;
|
433 |
|
long lastProcessed = -1L;
|
434 |
|
|
435 |
|
synchronized (writeLock)
|
436 |
|
{
|
437 |
|
markerID = lastAddedID;
|
438 |
|
lastProcessed = lastProcessedID;
|
439 |
|
}
|
440 |
|
|
441 |
|
synchronized (readLock)
|
442 |
|
{
|
443 |
|
while (lastProcessed < markerID)
|
444 |
|
{
|
445 |
|
try
|
446 |
|
{
|
447 |
|
writeLock.notify();
|
448 |
|
readLock.wait();
|
449 |
|
}
|
450 |
|
catch (InterruptedException exc)
|
451 |
|
{
|
452 |
|
if (done)
|
453 |
|
{
|
454 |
|
break;
|
455 |
|
}
|
456 |
|
}
|
457 |
|
|
458 |
|
synchronized (writeLock)
|
459 |
|
{
|
460 |
|
lastProcessed = lastProcessedID;
|
461 |
|
}
|
462 |
|
}
|
463 |
|
}
|
464 |
|
}
|
465 |
|
|
466 |
|
/**
|
467 |
|
* Set a flag to terminate the queue, removing any pending events which have not yet been
|
468 |
|
* processed. The worker thread is interrupted.
|
469 |
|
* <p>
|
470 |
|
* This method should invoked when the primary database associated with the enclosing lock
|
471 |
|
* table updater is deregistered, as part of cleaning up the associated metadata database.
|
472 |
|
*/
|
473 |
|
void terminate()
|
474 |
|
{
|
475 |
|
synchronized (writeLock)
|
476 |
|
{
|
477 |
|
done = true;
|
478 |
|
eventQueue.clear();
|
479 |
|
workThread.interrupt();
|
480 |
|
}
|
481 |
|
}
|
482 |
|
|
483 |
|
/**
|
484 |
|
* Process all pending record lock events in the queue. Events are read and removed from the
|
485 |
|
* queue synchronously, such that no new events may be added during the read/remove
|
486 |
|
* operation. However, they are then processed asynchronously, so new events may be added
|
487 |
|
* while lock metadata is being updated.
|
488 |
|
*/
|
489 |
|
private void processPendingEvents()
|
490 |
|
{
|
491 |
|
RecordLockEvent[] pending = null;
|
492 |
|
|
493 |
|
synchronized (readLock)
|
494 |
|
{
|
495 |
|
// collect all pending events (if any)
|
496 |
|
synchronized (writeLock)
|
497 |
|
{
|
498 |
|
int size = eventQueue.size();
|
499 |
|
if (size > 0)
|
500 |
|
{
|
501 |
|
pending = new RecordLockEvent[size];
|
502 |
|
Iterator<RecordLockEvent> iter = eventQueue.iterator();
|
503 |
|
for (int i = 0; iter.hasNext(); i++)
|
504 |
|
{
|
505 |
|
pending[i] = iter.next();
|
506 |
|
iter.remove();
|
507 |
|
}
|
508 |
|
}
|
509 |
|
}
|
510 |
|
|
511 |
|
// process pending events (if any); note that new events can be added to the queue
|
512 |
|
// while we are doing this
|
513 |
|
if (pending != null)
|
514 |
|
{
|
515 |
|
int len = pending.length;
|
516 |
|
for (int i = 0; i < len; i++)
|
517 |
|
{
|
518 |
|
processEvent(pending[i]);
|
519 |
|
}
|
520 |
|
|
521 |
|
synchronized (writeLock)
|
522 |
|
{
|
523 |
|
lastProcessedID = pending[len - 1].getEventID();
|
524 |
|
}
|
525 |
|
}
|
526 |
|
|
527 |
|
// give any threads waiting in forceFlush a chance to proceed
|
528 |
|
readLock.notifyAll();
|
529 |
|
}
|
530 |
|
}
|
531 |
|
|
532 |
|
/**
|
533 |
|
* Process an individual record lock event, updating the metadata lock table as appropriate.
|
534 |
|
* If the lock event represents a newly acquired lock, create a new lock record, otherwise
|
535 |
|
* find the existing lock record associated with the event, which must represent a change of
|
536 |
|
* the state of an existing lock. If that change of state represents a lock release, delete
|
537 |
|
* the associated lock record from the lock table.
|
538 |
|
*
|
539 |
|
* @param event
|
540 |
|
* Record lock event describing a new lock or a change in state of an existing
|
541 |
|
* lock.
|
542 |
|
*/
|
543 |
|
private void processEvent(RecordLockEvent event)
|
544 |
|
{
|
545 |
|
Record dmo = null;
|
546 |
|
boolean commit = false;
|
547 |
|
|
|
441 |
lockEvents.incrementAndGet();
|
|
442 |
guard.readLock().lock();
|
548 |
443 |
try
|
549 |
444 |
{
|
550 |
|
// DBG: System.out.println("<- " + event);
|
551 |
|
commit = persistence.beginTransaction();
|
552 |
|
|
553 |
|
LockType oldType = event.getOldType();
|
554 |
|
LockType newType = event.getNewType();
|
555 |
|
|
556 |
|
if (LockType.NONE.equals(oldType))
|
557 |
|
{
|
558 |
|
dmo = createRecord(event);
|
559 |
|
}
|
560 |
|
else
|
561 |
|
{
|
562 |
|
dmo = findRecord(event);
|
563 |
|
}
|
564 |
|
|
565 |
|
if (LockType.NONE.equals(newType))
|
566 |
|
{
|
567 |
|
persistence.delete(dmo);
|
568 |
|
}
|
569 |
|
else
|
570 |
|
{
|
571 |
|
String code = newType.isExclusive() ? "X" : "S"; // TODO: get this right
|
572 |
|
if (oldType.isShare())
|
573 |
|
{
|
574 |
|
code += " U"; // an upgrade
|
575 |
|
}
|
576 |
|
// From ABL online manual: values of flags field specify:
|
577 |
|
// S: a share lock,
|
578 |
|
// X: an exclusive lock,
|
579 |
|
// U: a lock upgraded from share to exclusive,
|
580 |
|
// L: a lock in limbo,
|
581 |
|
// Q: a queued lock,
|
582 |
|
// K: a lock kept across transaction end boundary,
|
583 |
|
// J: a lock is part of a JTA transaction,
|
584 |
|
// C: a lock is in create mode for JTA,
|
585 |
|
// E: a lock wait timeout has expired on this queued lock.
|
586 |
|
proxy.setLockFlags(new character(code));
|
587 |
|
|
588 |
|
boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
|
589 |
|
proxy.setLockType(new character(isTable ? "TAB" : "REC"));
|
590 |
|
|
591 |
|
persistence.save(dmo, dmo.primaryKey());
|
592 |
|
}
|
593 |
|
|
594 |
|
persistence.flush();
|
595 |
|
|
596 |
|
if (commit)
|
597 |
|
{
|
598 |
|
commit = false; // to avoid double-rollback in catch block if commit fails
|
599 |
|
persistence.commit();
|
600 |
|
}
|
601 |
|
|
602 |
|
if (log.isLoggable(Level.FINEST))
|
603 |
|
{
|
604 |
|
log.log(Level.FINEST, event.toString());
|
605 |
|
}
|
606 |
|
}
|
607 |
|
catch (Exception exc)
|
608 |
|
{
|
609 |
|
try
|
610 |
|
{
|
611 |
|
if (commit)
|
612 |
|
{
|
613 |
|
persistence.rollback();
|
614 |
|
}
|
615 |
|
}
|
616 |
|
catch (PersistenceException pe)
|
617 |
|
{
|
618 |
|
// error already logged in finally block
|
619 |
|
}
|
620 |
|
finally
|
621 |
|
{
|
622 |
|
if (log.isLoggable(Level.SEVERE))
|
623 |
|
{
|
624 |
|
String msg = String.format("Error updating _lock table for %s (%s)",
|
625 |
|
persistence.getDatabase().toString(),
|
626 |
|
event.toString());
|
627 |
|
log.log(Level.SEVERE, msg, exc);
|
628 |
|
}
|
|
445 |
mapVersion.incrementAndGet();
|
|
446 |
LockRecId lrId = new LockRecId(event);
|
|
447 |
if (event.getNewType() == LockType.NONE )
|
|
448 |
{
|
|
449 |
MinimalLockImpl ml = locks.remove(lrId);
|
|
450 |
if (ml != null && ml.persisted.get() >= 0)
|
|
451 |
{
|
|
452 |
// lock was already persisted, VST cleanup is required
|
|
453 |
ml.guardedUpdate(event);
|
|
454 |
executor.execute(() -> {
|
|
455 |
try
|
|
456 |
{
|
|
457 |
if (ml.persist())
|
|
458 |
{
|
|
459 |
deletesFromVST.incrementAndGet();
|
|
460 |
}
|
|
461 |
}
|
|
462 |
catch(Throwable t)
|
|
463 |
{
|
|
464 |
log.log(Level.WARNING, "Cannot delete lock data for " + ml.eventString, t);
|
|
465 |
}
|
|
466 |
});
|
|
467 |
}
|
|
468 |
return;
|
|
469 |
}
|
|
470 |
MinimalLockImpl ml = locks.computeIfAbsent(lrId, k -> new MinimalLockImpl(event));
|
|
471 |
if (!ml.version.compareAndSet(-1, 0)) // not a new lock, should be updated
|
|
472 |
{
|
|
473 |
ml.guardedUpdate(event);
|
629 |
474 |
}
|
630 |
475 |
}
|
631 |
476 |
finally
|
632 |
477 |
{
|
633 |
|
if (dmo != null)
|
634 |
|
{
|
635 |
|
RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
|
636 |
|
dmo.primaryKey());
|
637 |
|
try
|
638 |
|
{
|
639 |
|
persistence.lock(LockType.NONE, metaIdent, true);
|
640 |
|
}
|
641 |
|
catch (LockUnavailableException exc)
|
642 |
|
{
|
643 |
|
// won't be thrown from lock release
|
644 |
|
}
|
645 |
|
}
|
|
478 |
guard.readLock().unlock();
|
646 |
479 |
}
|
647 |
480 |
}
|
648 |
481 |
|
... | ... | |
655 |
488 |
*
|
656 |
489 |
* @return See above.
|
657 |
490 |
*/
|
658 |
|
private long getLockRecordID(RecordLockEvent event)
|
659 |
|
throws PersistenceException
|
|
491 |
public long getLockRecordID(RecordLockEvent event)
|
660 |
492 |
{
|
661 |
493 |
Long recordID = event.getIdentifier().getRecordID();
|
662 |
494 |
long recID = (recordID != null) ? (Long) recordID : -getFileNum(event);
|
... | ... | |
665 |
497 |
}
|
666 |
498 |
|
667 |
499 |
/**
|
668 |
|
* Find the metadata lock record associated with the user ID and lock record ID specified by
|
669 |
|
* the given record lock event.
|
670 |
|
*
|
671 |
|
* @param event
|
672 |
|
* Record lock event.
|
673 |
|
*
|
674 |
|
* @return Metadata lock record, if found.
|
675 |
|
*
|
676 |
|
* @throws PersistenceException
|
677 |
|
* if a lock record cannot be found for the given event, or if there is an error
|
678 |
|
* executing the query.
|
679 |
|
*/
|
680 |
|
private Record findRecord(RecordLockEvent event)
|
681 |
|
throws PersistenceException
|
682 |
|
{
|
683 |
|
long lockRec = getLockRecordID(event);
|
684 |
|
int lockUsr = event.getUserid().intValue();
|
685 |
|
Object[] args = new Object[] { lockRec, lockUsr };
|
686 |
|
|
687 |
|
Long id = persistence.getSingleSQLResult(SQL_LOCK, args);
|
688 |
|
Record dmo = null;
|
689 |
|
|
690 |
|
if (id != null)
|
691 |
|
{
|
692 |
|
RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), id);
|
693 |
|
persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
|
694 |
|
dmo = persistence.quickLoad(metaIdent);
|
695 |
|
}
|
696 |
|
|
697 |
|
if (dmo == null)
|
698 |
|
{
|
699 |
|
throw new PersistenceException("No lock record found for lock event: " + event);
|
700 |
|
}
|
701 |
|
|
702 |
|
handler.setDelegate(dmo);
|
703 |
|
|
704 |
|
return dmo;
|
705 |
|
}
|
706 |
|
|
707 |
|
/**
|
708 |
|
* Create a metadata lock record for the given record lock event.
|
709 |
|
*
|
710 |
|
* @param event
|
711 |
|
* Event which specifies information to be stored in the lock record.
|
712 |
|
*
|
713 |
|
* @return New lock record.
|
714 |
|
*
|
715 |
|
* @throws PersistenceException
|
716 |
|
* if there is any error querying related metadata, or if there is a reflection
|
717 |
|
* error instantiating the DMO.
|
718 |
|
*/
|
719 |
|
private Record createRecord(RecordLockEvent event)
|
720 |
|
throws PersistenceException
|
721 |
|
{
|
722 |
|
Record dmo = null;
|
723 |
|
|
724 |
|
try
|
725 |
|
{
|
726 |
|
// create new DMO instance and back the proxy with it
|
727 |
|
dmo = dmoClass.newInstance();
|
728 |
|
dmo.initialize(null, true);
|
729 |
|
handler.setDelegate(dmo);
|
730 |
|
|
731 |
|
// assign primary key
|
732 |
|
Long key = nextPrimaryKey++;
|
733 |
|
dmo.primaryKey(key);
|
734 |
|
|
735 |
|
// get file number for table
|
736 |
|
Integer fileNum = getFileNum(event);
|
737 |
|
|
738 |
|
// lock meta record for writing
|
739 |
|
RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), dmo.primaryKey());
|
740 |
|
persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
|
741 |
|
|
742 |
|
// set user name and user id
|
743 |
|
String subject = event.getOsUserName();
|
744 |
|
if (subject == null)
|
745 |
|
{
|
746 |
|
subject = event.getLocker().getSubject();
|
747 |
|
}
|
748 |
|
character userName = new character(subject);
|
749 |
|
proxy.setLockName(userName);
|
750 |
|
proxy.setLockUsr(event.getUserid());
|
751 |
|
|
752 |
|
// assign other properties that will not change with new events
|
753 |
|
proxy.setLockId(new int64(key)); // re-use primary key
|
754 |
|
proxy.setLockTable(new integer(fileNum));
|
755 |
|
proxy.setLockRecid(new int64(getLockRecordID(event)));
|
756 |
|
|
757 |
|
// In ProgressOE, the locking table is divided into chains anchored in a hash table.
|
758 |
|
// These chains are provided for fast lookup of record locks by Rec-id.
|
759 |
|
// FWD OTOH, relies on underlying database for recid/rowid lookup so the chain is at
|
760 |
|
// least irrelevant. In order to have a substitute and keep things simple, we will
|
761 |
|
// assume that each table has a single chain and its value is computed as an fixed
|
762 |
|
// offset from the table ID.
|
763 |
|
proxy.setLockChain(new integer(fileNum + CHAIN_OFFSET));
|
764 |
|
}
|
765 |
|
catch (InstantiationException | IllegalAccessException exc)
|
766 |
|
{
|
767 |
|
String msg = String.format("Error creating meta lock record for event %s",
|
768 |
|
event.toString());
|
769 |
|
|
770 |
|
throw new PersistenceException(msg, exc);
|
771 |
|
}
|
772 |
|
|
773 |
|
return dmo;
|
774 |
|
}
|
775 |
|
|
776 |
|
/**
|
777 |
500 |
* Get the file number associated with a lock event.
|
778 |
501 |
*
|
779 |
502 |
* @param event
|
... | ... | |
782 |
505 |
* @return File number for the table associated with the event.
|
783 |
506 |
*/
|
784 |
507 |
private Integer getFileNum(RecordLockEvent event)
|
785 |
|
throws PersistenceException
|
786 |
508 |
{
|
787 |
509 |
RecordIdentifier<String> ident = event.getIdentifier();
|
788 |
510 |
String table = ident.getTable();
|
789 |
|
Integer fileNum = fileNums.get(table);
|
790 |
|
if (fileNum == null)
|
|
511 |
Integer fileNum;
|
|
512 |
if (MetadataManager.inUse(MetadataManager._FILE))
|
791 |
513 |
{
|
792 |
|
if (MetadataManager.inUse(MetadataManager._FILE))
|
793 |
|
{
|
794 |
|
// converted table name is stored in MetaFile.userMisc
|
795 |
|
Object[] args = new Object[] { table };
|
796 |
|
fileNum = persistence.getSingleSQLResult(SQL_FILENUM, args);
|
797 |
|
|
798 |
|
if (fileNum == null)
|
799 |
|
{
|
800 |
|
String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
|
801 |
|
throw new PersistenceException(msg);
|
802 |
|
}
|
803 |
|
}
|
804 |
|
else
|
805 |
|
{
|
806 |
|
// the _File metadata is not available
|
807 |
|
fileNum = 0;
|
808 |
|
}
|
|
514 |
// converted table name is stored in MetaFile.userMisc
|
|
515 |
fileNum = MetadataManager.getFileNum(primary, table);
|
809 |
516 |
|
810 |
|
// cache this to avoid future query by userMisc, which is not indexed
|
811 |
|
fileNums.put(table, fileNum);
|
812 |
|
}
|
813 |
|
|
|
517 |
if (fileNum == null)
|
|
518 |
{
|
|
519 |
String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
|
|
520 |
throw new IllegalStateException(msg);
|
|
521 |
}
|
|
522 |
}
|
|
523 |
else
|
|
524 |
{
|
|
525 |
// the _File metadata is not available
|
|
526 |
fileNum = 0;
|
|
527 |
}
|
814 |
528 |
return fileNum;
|
815 |
529 |
}
|
816 |
530 |
|
... | ... | |
855 |
569 |
this.delegate = delegate;
|
856 |
570 |
}
|
857 |
571 |
}
|
|
572 |
|
|
573 |
/** In-memory holder for the _Lock VST data*/
|
|
574 |
private class MinimalLockImpl
|
|
575 |
{
|
|
576 |
/** Lock guard */
|
|
577 |
public final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();
|
|
578 |
|
|
579 |
/** Lock version */
|
|
580 |
public final AtomicLong version = new AtomicLong(-1);
|
|
581 |
|
|
582 |
/** Lock version last persisted */
|
|
583 |
public final AtomicLong persisted = new AtomicLong(-1);
|
|
584 |
|
|
585 |
/** Flag indicating that the corresponding record was already deleted in VST */
|
|
586 |
public volatile boolean deleted = false;
|
|
587 |
|
|
588 |
/** Primary key */
|
|
589 |
public final long key;
|
|
590 |
|
|
591 |
/** Lock Id */
|
|
592 |
public final int64 lockId;
|
|
593 |
|
|
594 |
/** User */
|
|
595 |
public final integer lockUsr;
|
|
596 |
|
|
597 |
/** Lock name */
|
|
598 |
public final character lockName;
|
|
599 |
|
|
600 |
/** Table fileNum */
|
|
601 |
public final integer lockTable;
|
|
602 |
|
|
603 |
/** Record Id */
|
|
604 |
public final int64 lockRecid;
|
|
605 |
|
|
606 |
/** Chain */
|
|
607 |
public final integer lockChain;
|
|
608 |
|
|
609 |
/** Old lock type */
|
|
610 |
private LockType oldType;
|
|
611 |
|
|
612 |
/** New lock type */
|
|
613 |
private LockType newType;
|
|
614 |
|
|
615 |
/** Lock type string */
|
|
616 |
private character lockType;
|
|
617 |
|
|
618 |
/** Lock flags */
|
|
619 |
private character lockFlags;
|
|
620 |
|
|
621 |
/** Event string representation (for logging) */
|
|
622 |
private String eventString;
|
|
623 |
|
|
624 |
/**
|
|
625 |
* Constructor
|
|
626 |
*
|
|
627 |
* @param event
|
|
628 |
* Lock event
|
|
629 |
*/
|
|
630 |
public MinimalLockImpl(RecordLockEvent event)
|
|
631 |
{
|
|
632 |
key = nextPrimaryKey.incrementAndGet();
|
|
633 |
|
|
634 |
// set user name and user id
|
|
635 |
String subject = event.getOsUserName();
|
|
636 |
if (subject == null)
|
|
637 |
{
|
|
638 |
subject = event.getLocker().getSubject();
|
|
639 |
}
|
|
640 |
character userName = new character(subject);
|
|
641 |
lockName = userName;
|
|
642 |
lockUsr = event.getUserid();
|
|
643 |
|
|
644 |
// get file number for table
|
|
645 |
Integer fileNum = getFileNum(event);
|
|
646 |
|
|
647 |
// assign other properties that will not change with new events
|
|
648 |
lockId = new int64(key); // re-use primary key
|
|
649 |
lockTable = new integer(fileNum);
|
|
650 |
lockRecid = new int64(getLockRecordID(event));
|
|
651 |
|
|
652 |
// In ProgressOE, the locking table is divided into chains anchored in a hash table.
|
|
653 |
// These chains are provided for fast lookup of record locks by Rec-id.
|
|
654 |
// FWD OTOH, relies on underlying database for recid/rowid lookup so the chain is at
|
|
655 |
// least irrelevant. In order to have a substitute and keep things simple, we will
|
|
656 |
// assume that each table has a single chain and its value is computed as an fixed
|
|
657 |
// offset from the table ID.
|
|
658 |
lockChain = new integer(fileNum + CHAIN_OFFSET);
|
|
659 |
|
|
660 |
update(event);
|
|
661 |
}
|
|
662 |
|
|
663 |
/**
|
|
664 |
* Guarded update holder with event data
|
|
665 |
* @param event
|
|
666 |
* lock event
|
|
667 |
*
|
|
668 |
*/
|
|
669 |
public void guardedUpdate(RecordLockEvent event)
|
|
670 |
{
|
|
671 |
guard.writeLock().lock();
|
|
672 |
try
|
|
673 |
{
|
|
674 |
update(event);
|
|
675 |
version.incrementAndGet();
|
|
676 |
}
|
|
677 |
finally
|
|
678 |
{
|
|
679 |
guard.writeLock().unlock();
|
|
680 |
}
|
|
681 |
}
|
|
682 |
|
|
683 |
/**
|
|
684 |
* Update holder with event data
|
|
685 |
* @param event
|
|
686 |
* lock event
|
|
687 |
*
|
|
688 |
*/
|
|
689 |
public void update(RecordLockEvent event)
|
|
690 |
{
|
|
691 |
this.oldType = event.getOldType();
|
|
692 |
this.newType = event.getNewType();
|
|
693 |
String code = newType.isExclusive() ? "X" : "S"; // TODO: get this right
|
|
694 |
if (oldType.isShare())
|
|
695 |
{
|
|
696 |
code += " U"; // an upgrade
|
|
697 |
}
|
|
698 |
// From ABL online manual: values of flags field specify:
|
|
699 |
// S: a share lock,
|
|
700 |
// X: an exclusive lock,
|
|
701 |
// U: a lock upgraded from share to exclusive,
|
|
702 |
// L: a lock in limbo,
|
|
703 |
// Q: a queued lock,
|
|
704 |
// K: a lock kept across transaction end boundary,
|
|
705 |
// J: a lock is part of a JTA transaction,
|
|
706 |
// C: a lock is in create mode for JTA,
|
|
707 |
// E: a lock wait timeout has expired on this queued lock.
|
|
708 |
this.lockFlags = new character(code);
|
|
709 |
|
|
710 |
boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
|
|
711 |
this.lockType = new character(isTable ? "TAB" : "REC");
|
|
712 |
this.eventString = String.format("%s: %d[%d, %d]", event.toString(),
|
|
713 |
key, lockRecid.longValue(), lockUsr.intValue());
|
|
714 |
}
|
|
715 |
|
|
716 |
/** Create record in the _Lock VST */
|
|
717 |
private Record createRecord() throws PersistenceException
|
|
718 |
{
|
|
719 |
Record dmo = null;
|
|
720 |
|
|
721 |
try
|
|
722 |
{
|
|
723 |
// create new DMO instance and back the proxy with it
|
|
724 |
dmo = dmoClass.newInstance();
|
|
725 |
dmo.initialize(null, true);
|
|
726 |
handler.setDelegate(dmo);
|
|
727 |
|
|
728 |
// assign primary key
|
|
729 |
dmo.primaryKey(key);
|
|
730 |
// lock meta record for writing
|
|
731 |
RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
|
|
732 |
dmo.primaryKey());
|
|
733 |
persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
|
|
734 |
proxy.setLockName(lockName);
|
|
735 |
proxy.setLockUsr(lockUsr);
|
|
736 |
proxy.setLockId(lockId); // re-use primary key
|
|
737 |
proxy.setLockTable(lockTable);
|
|
738 |
proxy.setLockRecid(lockRecid);
|
|
739 |
proxy.setLockChain(lockChain);
|
|
740 |
}
|
|
741 |
catch (InstantiationException | IllegalAccessException exc)
|
|
742 |
{
|
|
743 |
String msg = String.format("Error creating meta lock record for event %s",
|
|
744 |
eventString);
|
|
745 |
|
|
746 |
throw new PersistenceException(msg, exc);
|
|
747 |
}
|
|
748 |
return dmo;
|
|
749 |
}
|
|
750 |
|
|
751 |
|
|
752 |
/** Find record in the _Lock VST */
|
|
753 |
private Record findRecord() throws PersistenceException
|
|
754 |
{
|
|
755 |
Long id = persistence.getSingleSQLResult(SQL_LOCK, new Object[] { key });
|
|
756 |
Record dmo = null;
|
|
757 |
|
|
758 |
if (id != null)
|
|
759 |
{
|
|
760 |
RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
|
|
761 |
id);
|
|
762 |
persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
|
|
763 |
dmo = persistence.quickLoad(metaIdent);
|
|
764 |
}
|
|
765 |
|
|
766 |
if (dmo == null)
|
|
767 |
{
|
|
768 |
throw new PersistenceException(
|
|
769 |
"No lock record found for lock event: " + eventString);
|
|
770 |
}
|
|
771 |
|
|
772 |
handler.setDelegate(dmo);
|
|
773 |
|
|
774 |
return dmo;
|
|
775 |
}
|
|
776 |
|
|
777 |
/**
|
|
778 |
* Guarded persist lock data in the _Lock VST.
|
|
779 |
* @return <code>true</code> if success
|
|
780 |
*/
|
|
781 |
public boolean persist()
|
|
782 |
{
|
|
783 |
guard.writeLock().lock();
|
|
784 |
try
|
|
785 |
{
|
|
786 |
return doPersist();
|
|
787 |
}
|
|
788 |
finally
|
|
789 |
{
|
|
790 |
guard.writeLock().unlock();
|
|
791 |
}
|
|
792 |
}
|
|
793 |
|
|
794 |
/**
|
|
795 |
* Persist lock data in the _Lock VST.
|
|
796 |
* @return <code>true</code> if success
|
|
797 |
*/
|
|
798 |
private boolean doPersist()
|
|
799 |
{
|
|
800 |
if (persisted.get() >= version.get() || deleted)
|
|
801 |
{
|
|
802 |
return false;
|
|
803 |
}
|
|
804 |
Record dmo = null;
|
|
805 |
boolean commit = false;
|
|
806 |
|
|
807 |
try
|
|
808 |
{
|
|
809 |
commit = persistence.beginTransaction();
|
|
810 |
|
|
811 |
if (persisted.get() < 0)
|
|
812 |
{
|