cache clearing through cluster messages

deepsource-autofix-76c6eb20
Dan Gowans 2023-03-27 10:10:50 -04:00
parent 60f0a13959
commit 1401786254
15 changed files with 150 additions and 150 deletions

View File

@ -7,20 +7,37 @@ import * as configFunctions from '../helpers/functions.config.js';
import exitHook from 'exit-hook';
import ntfyPublish from '@cityssm/ntfy-publish';
import Debug from 'debug';
const debug = Debug('lot-occupancy-system:www');
const debug = Debug(`lot-occupancy-system:www:${process.pid}`);
const directoryName = dirname(fileURLToPath(import.meta.url));
const processCount = Math.min(configFunctions.getProperty('application.maximumProcesses'), os.cpus().length);
process.title =
configFunctions.getProperty('application.applicationName') + ' (Primary)';
debug(`Primary pid: ${process.pid}`);
debug(`Primary title: ${process.title}`);
debug(`Launching ${processCount} processes`);
const clusterSettings = {
exec: directoryName + '/wwwProcess.js'
};
cluster.setupPrimary(clusterSettings);
const activeWorkers = new Map();
for (let index = 0; index < processCount; index += 1) {
cluster.fork();
const worker = cluster.fork();
activeWorkers.set(worker.process.pid, worker);
}
cluster.on('message', (worker, message) => {
if (message?.messageType === 'clearCache') {
for (const [pid, worker] of activeWorkers.entries()) {
if (worker === undefined || pid === message.pid) {
continue;
}
debug('Relaying message to workers');
worker.send(message);
}
}
});
cluster.on('exit', (worker, code, signal) => {
debug(`Worker ${worker.process.pid.toString()} has been killed`);
activeWorkers.delete(worker.process.pid);
debug('Starting another worker');
cluster.fork();
});

View File

@ -12,8 +12,10 @@ import exitHook from 'exit-hook'
import ntfyPublish from '@cityssm/ntfy-publish'
import type * as ntfyTypes from '@cityssm/ntfy-publish/types'
import type { WorkerMessage } from '../types/applicationTypes'
import Debug from 'debug'
const debug = Debug('lot-occupancy-system:www')
const debug = Debug(`lot-occupancy-system:www:${process.pid}`)
const directoryName = dirname(fileURLToPath(import.meta.url))
@ -22,7 +24,11 @@ const processCount = Math.min(
os.cpus().length
)
process.title =
configFunctions.getProperty('application.applicationName') + ' (Primary)'
debug(`Primary pid: ${process.pid}`)
debug(`Primary title: ${process.title}`)
debug(`Launching ${processCount} processes`)
const clusterSettings = {
@ -31,12 +37,30 @@ const clusterSettings = {
cluster.setupPrimary(clusterSettings)
const activeWorkers = new Map<number, any>()
for (let index = 0; index < processCount; index += 1) {
cluster.fork()
const worker = cluster.fork()
activeWorkers.set(worker.process.pid!, worker)
}
cluster.on('message', (worker, message: WorkerMessage) => {
if (message?.messageType === 'clearCache') {
for (const [pid, worker] of activeWorkers.entries()) {
if (worker === undefined || pid === message.pid) {
continue
}
debug('Relaying message to workers')
worker.send(message)
}
}
})
cluster.on('exit', (worker, code, signal) => {
debug(`Worker ${worker.process.pid!.toString()} has been killed`)
activeWorkers.delete(worker.process.pid!)
debug('Starting another worker')
cluster.fork()
})

View File

@ -29,6 +29,7 @@ function onListening(server) {
debug('HTTP Listening on ' + bind);
}
}
process.title = configFunctions.getProperty('application.applicationName') + ' (Worker)';
const httpPort = configFunctions.getProperty('application.httpPort');
const httpServer = http.createServer(app);
httpServer.listen(httpPort);

View File

@ -58,6 +58,8 @@ function onListening(server: http.Server): void {
* Initialize HTTP
*/
process.title = configFunctions.getProperty('application.applicationName') + ' (Worker)'
const httpPort = configFunctions.getProperty('application.httpPort')
const httpServer = http.createServer(app)

View File

@ -0,0 +1,33 @@
[Home](https://cityssm.github.io/lot-occupancy-system/)
[Help](https://cityssm.github.io/lot-occupancy-system/docs/)
# Cemetery Management System Workflow - Newly Deceased
_The following workflow describes a process that can be used when the Lot Occupancy System is used
as a Cemetery Management System._
## Step 1: Search for a Related Preneed Occupancy Record
![Occupancy Search](images/lotOccupancySearch.png)
If the deceased purchased preneed services, find them now.
![Occupancy View](images/lotOccupancyView.png)
It is important to note what services have been paid for,
and who is entitled to the services.
## Step 2: Create the New Interment or Cremation Occupancy Record
![Occupancy Edit - More Options](images/lotOccupancyEdit-moreOptions.png)
If a preneed occupancy record exists, you can save time by copying the preneed record as a new record.
If no preneed occupancy record exists, a new occupancy record should be created.
## Step 3: Create a Work Order Associated with the Occupancy Record
Ensure the necessary milestones are included.
## Step 4: Complete the Work Order

View File

@ -18,4 +18,4 @@ export declare function getWorkOrderTypeById(workOrderTypeId: number): Promise<r
export declare function getWorkOrderMilestoneTypes(): Promise<recordTypes.WorkOrderMilestoneType[]>;
export declare function getWorkOrderMilestoneTypeById(workOrderMilestoneTypeId: number): Promise<recordTypes.WorkOrderMilestoneType | undefined>;
export declare function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrderMilestoneTypeString: string): Promise<recordTypes.WorkOrderMilestoneType | undefined>;
export declare function clearCacheByTableName(tableName: string): void;
export declare function clearCacheByTableName(tableName: string, relayMessage?: boolean): void;

View File

@ -1,3 +1,4 @@
import cluster from 'node:cluster';
import * as configFunctions from './functions.config.js';
import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js';
import { getLotStatuses as getLotStatusesFromDatabase } from './lotOccupancyDB/getLotStatuses.js';
@ -6,9 +7,8 @@ import { getOccupancyTypes as getOccupancyTypesFromDatabase } from './lotOccupan
import { getOccupancyTypeFields as getOccupancyTypeFieldsFromDatabase } from './lotOccupancyDB/getOccupancyTypeFields.js';
import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupancyDB/getWorkOrderTypes.js';
import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js';
import { getConfigTableMaxTimeMillis } from './lotOccupancyDB/getConfigTableMaxTimeMillis.js';
import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async';
import { asyncExitHook } from 'exit-hook';
import Debug from 'debug';
const debug = Debug(`lot-occupancy-system:functions.cache:${process.pid}`);
let lotOccupantTypes;
export async function getLotOccupantTypes() {
if (lotOccupantTypes === undefined) {
@ -163,7 +163,7 @@ export async function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrde
function clearWorkOrderMilestoneTypesCache() {
workOrderMilestoneTypes = undefined;
}
export function clearCacheByTableName(tableName) {
export function clearCacheByTableName(tableName, relayMessage = true) {
switch (tableName) {
case 'LotOccupantTypes': {
clearLotOccupantTypesCache();
@ -193,26 +193,23 @@ export function clearCacheByTableName(tableName) {
break;
}
}
}
function clearAllCaches() {
clearLotOccupantTypesCache();
clearLotStatusesCache();
clearLotTypesCache();
clearOccupancyTypesCache();
clearWorkOrderMilestoneTypesCache();
clearWorkOrderTypesCache();
}
let configTimeMillis = 0;
async function checkCacheIntegrity() {
const timeMillis = await getConfigTableMaxTimeMillis();
if (timeMillis > configTimeMillis) {
configTimeMillis = timeMillis;
clearAllCaches();
try {
if (relayMessage && cluster.isWorker) {
const workerMessage = {
messageType: 'clearCache',
tableName,
timeMillis: Date.now(),
pid: process.pid
};
debug(`Sending clear cache from worker: ${tableName}`);
process.send(workerMessage);
}
}
const cacheTimer = setIntervalAsync(checkCacheIntegrity, 10 * 60 * 1000);
asyncExitHook(async () => {
await clearIntervalAsync(cacheTimer);
}, {
minimumWait: 250
catch { }
}
process.on('message', (message) => {
if (message.messageType === 'clearCache' && message.pid !== process.pid) {
debug(`Clearing cache: ${message.tableName}`);
clearCacheByTableName(message.tableName, false);
}
});

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/indent */
import cluster from 'node:cluster'
import * as configFunctions from './functions.config.js'
import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js'
@ -15,12 +17,11 @@ import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupan
import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js'
import { getConfigTableMaxTimeMillis } from './lotOccupancyDB/getConfigTableMaxTimeMillis.js'
import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async'
import { asyncExitHook } from 'exit-hook'
import type * as recordTypes from '../types/recordTypes'
import type { WorkerMessage } from '../types/applicationTypes'
import Debug from 'debug'
const debug = Debug(`lot-occupancy-system:functions.cache:${process.pid}`)
/*
* Lot Occupant Types
@ -301,7 +302,10 @@ function clearWorkOrderMilestoneTypesCache(): void {
workOrderMilestoneTypes = undefined
}
export function clearCacheByTableName(tableName: string): void {
export function clearCacheByTableName(
tableName: string,
relayMessage = true
): void {
switch (tableName) {
case 'LotOccupantTypes': {
clearLotOccupantTypesCache()
@ -336,39 +340,26 @@ export function clearCacheByTableName(tableName: string): void {
break
}
}
try {
if (relayMessage && cluster.isWorker) {
const workerMessage: WorkerMessage = {
messageType: 'clearCache',
tableName,
timeMillis: Date.now(),
pid: process.pid
}
function clearAllCaches(): void {
clearLotOccupantTypesCache()
clearLotStatusesCache()
clearLotTypesCache()
clearOccupancyTypesCache()
clearWorkOrderMilestoneTypesCache()
clearWorkOrderTypesCache()
debug(`Sending clear cache from worker: ${tableName}`)
process.send!(workerMessage)
}
} catch {}
}
/*
* Config Time Millis
*/
let configTimeMillis = 0
async function checkCacheIntegrity(): Promise<void> {
const timeMillis = await getConfigTableMaxTimeMillis()
if (timeMillis > configTimeMillis) {
configTimeMillis = timeMillis
clearAllCaches()
process.on('message', (message: WorkerMessage) => {
if (message.messageType === 'clearCache' && message.pid !== process.pid) {
debug(`Clearing cache: ${message.tableName}`)
clearCacheByTableName(message.tableName, false)
}
}
const cacheTimer = setIntervalAsync(checkCacheIntegrity, 10 * 60 * 1000)
asyncExitHook(
async () => {
await clearIntervalAsync(cacheTimer)
},
{
minimumWait: 250
}
)
})

View File

@ -1,2 +0,0 @@
export declare function getConfigTableMaxTimeMillis(): Promise<number>;
export default getConfigTableMaxTimeMillis;

View File

@ -1,34 +0,0 @@
import { acquireConnection } from './pool.js';
export async function getConfigTableMaxTimeMillis() {
const database = await acquireConnection();
const result = database
.prepare(`select max(timeMillis) as timeMillis from (
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotOccupantTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotStatuses
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypeFields
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypePrints
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from WorkOrderTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from WorkOrderMilestoneTypes
)`)
.get();
database.release();
return result?.timeMillis ?? 0;
}
export default getConfigTableMaxTimeMillis;

View File

@ -1,41 +0,0 @@
import { acquireConnection } from './pool.js'
export async function getConfigTableMaxTimeMillis(): Promise<number> {
const database = await acquireConnection()
const result = database
.prepare(
`select max(timeMillis) as timeMillis from (
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotOccupantTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotStatuses
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from LotTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypeFields
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from OccupancyTypePrints
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from WorkOrderTypes
UNION
select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis
from WorkOrderMilestoneTypes
)`
)
.get()
database.release()
return result?.timeMillis ?? 0
}
export default getConfigTableMaxTimeMillis

View File

@ -8,8 +8,8 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"start": "cross-env NODE_ENV=production node ./bin/www",
"dev:test": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:*,dynamics-gp:* TEST_DATABASES=true nodemon ./bin/www.js",
"start": "cross-env NODE_ENV=production node ./bin/www.js",
"dev:test": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:*,dynamics-gp:* TEST_DATABASES=true nodemon --inspect ./bin/www.js",
"dev:test:process": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:* TEST_DATABASES=true nodemon ./bin/wwwProcess.js",
"dev:live": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:* nodemon ./bin/www.js",
"cy:open": "cypress open --config-file cypress.config.js",

View File

@ -1,6 +1,6 @@
@import '@cityssm/bulma-webapp-css/cityssm';
@import 'bulma/sass/utilities/derived-variables';
@import 'bulma-calendar/src/sass/index';
@import 'bulma-calendar/src/sass';
@import '@cityssm/fa-glow/fa-glow';
$white: #fff;

6
types/applicationTypes.d.ts vendored 100644
View File

@ -0,0 +1,6 @@
export interface WorkerMessage {
messageType: 'clearCache';
tableName: string;
timeMillis: number;
pid: number;
}

View File

@ -0,0 +1,6 @@
export interface WorkerMessage {
messageType: 'clearCache'
tableName: string
timeMillis: number
pid: number
}