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 exitHook from 'exit-hook';
import ntfyPublish from '@cityssm/ntfy-publish'; import ntfyPublish from '@cityssm/ntfy-publish';
import Debug from 'debug'; 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 directoryName = dirname(fileURLToPath(import.meta.url));
const processCount = Math.min(configFunctions.getProperty('application.maximumProcesses'), os.cpus().length); const processCount = Math.min(configFunctions.getProperty('application.maximumProcesses'), os.cpus().length);
debug(`Primary pid: ${process.pid}`); process.title =
configFunctions.getProperty('application.applicationName') + ' (Primary)';
debug(`Primary pid: ${process.pid}`);
debug(`Primary title: ${process.title}`);
debug(`Launching ${processCount} processes`); debug(`Launching ${processCount} processes`);
const clusterSettings = { const clusterSettings = {
exec: directoryName + '/wwwProcess.js' exec: directoryName + '/wwwProcess.js'
}; };
cluster.setupPrimary(clusterSettings); cluster.setupPrimary(clusterSettings);
const activeWorkers = new Map();
for (let index = 0; index < processCount; index += 1) { 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) => { cluster.on('exit', (worker, code, signal) => {
debug(`Worker ${worker.process.pid.toString()} has been killed`); debug(`Worker ${worker.process.pid.toString()} has been killed`);
activeWorkers.delete(worker.process.pid);
debug('Starting another worker'); debug('Starting another worker');
cluster.fork(); cluster.fork();
}); });

View File

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

View File

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

View File

@ -58,6 +58,8 @@ function onListening(server: http.Server): void {
* Initialize HTTP * Initialize HTTP
*/ */
process.title = configFunctions.getProperty('application.applicationName') + ' (Worker)'
const httpPort = configFunctions.getProperty('application.httpPort') const httpPort = configFunctions.getProperty('application.httpPort')
const httpServer = http.createServer(app) 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 getWorkOrderMilestoneTypes(): Promise<recordTypes.WorkOrderMilestoneType[]>;
export declare function getWorkOrderMilestoneTypeById(workOrderMilestoneTypeId: number): Promise<recordTypes.WorkOrderMilestoneType | undefined>; export declare function getWorkOrderMilestoneTypeById(workOrderMilestoneTypeId: number): Promise<recordTypes.WorkOrderMilestoneType | undefined>;
export declare function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrderMilestoneTypeString: string): 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 * as configFunctions from './functions.config.js';
import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js'; import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js';
import { getLotStatuses as getLotStatusesFromDatabase } from './lotOccupancyDB/getLotStatuses.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 { getOccupancyTypeFields as getOccupancyTypeFieldsFromDatabase } from './lotOccupancyDB/getOccupancyTypeFields.js';
import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupancyDB/getWorkOrderTypes.js'; import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupancyDB/getWorkOrderTypes.js';
import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js'; import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js';
import { getConfigTableMaxTimeMillis } from './lotOccupancyDB/getConfigTableMaxTimeMillis.js'; import Debug from 'debug';
import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async'; const debug = Debug(`lot-occupancy-system:functions.cache:${process.pid}`);
import { asyncExitHook } from 'exit-hook';
let lotOccupantTypes; let lotOccupantTypes;
export async function getLotOccupantTypes() { export async function getLotOccupantTypes() {
if (lotOccupantTypes === undefined) { if (lotOccupantTypes === undefined) {
@ -163,7 +163,7 @@ export async function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrde
function clearWorkOrderMilestoneTypesCache() { function clearWorkOrderMilestoneTypesCache() {
workOrderMilestoneTypes = undefined; workOrderMilestoneTypes = undefined;
} }
export function clearCacheByTableName(tableName) { export function clearCacheByTableName(tableName, relayMessage = true) {
switch (tableName) { switch (tableName) {
case 'LotOccupantTypes': { case 'LotOccupantTypes': {
clearLotOccupantTypesCache(); clearLotOccupantTypesCache();
@ -193,26 +193,23 @@ export function clearCacheByTableName(tableName) {
break; break;
} }
} }
} try {
function clearAllCaches() { if (relayMessage && cluster.isWorker) {
clearLotOccupantTypesCache(); const workerMessage = {
clearLotStatusesCache(); messageType: 'clearCache',
clearLotTypesCache(); tableName,
clearOccupancyTypesCache(); timeMillis: Date.now(),
clearWorkOrderMilestoneTypesCache(); pid: process.pid
clearWorkOrderTypesCache(); };
} debug(`Sending clear cache from worker: ${tableName}`);
let configTimeMillis = 0; process.send(workerMessage);
async function checkCacheIntegrity() { }
const timeMillis = await getConfigTableMaxTimeMillis();
if (timeMillis > configTimeMillis) {
configTimeMillis = timeMillis;
clearAllCaches();
} }
catch { }
} }
const cacheTimer = setIntervalAsync(checkCacheIntegrity, 10 * 60 * 1000); process.on('message', (message) => {
asyncExitHook(async () => { if (message.messageType === 'clearCache' && message.pid !== process.pid) {
await clearIntervalAsync(cacheTimer); debug(`Clearing cache: ${message.tableName}`);
}, { clearCacheByTableName(message.tableName, false);
minimumWait: 250 }
}); });

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/indent */
import cluster from 'node:cluster'
import * as configFunctions from './functions.config.js' import * as configFunctions from './functions.config.js'
import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.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 { 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 * 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 * Lot Occupant Types
@ -301,7 +302,10 @@ function clearWorkOrderMilestoneTypesCache(): void {
workOrderMilestoneTypes = undefined workOrderMilestoneTypes = undefined
} }
export function clearCacheByTableName(tableName: string): void { export function clearCacheByTableName(
tableName: string,
relayMessage = true
): void {
switch (tableName) { switch (tableName) {
case 'LotOccupantTypes': { case 'LotOccupantTypes': {
clearLotOccupantTypesCache() clearLotOccupantTypesCache()
@ -336,39 +340,26 @@ export function clearCacheByTableName(tableName: string): void {
break break
} }
} }
try {
if (relayMessage && cluster.isWorker) {
const workerMessage: WorkerMessage = {
messageType: 'clearCache',
tableName,
timeMillis: Date.now(),
pid: process.pid
}
debug(`Sending clear cache from worker: ${tableName}`)
process.send!(workerMessage)
}
} catch {}
} }
function clearAllCaches(): void { process.on('message', (message: WorkerMessage) => {
clearLotOccupantTypesCache() if (message.messageType === 'clearCache' && message.pid !== process.pid) {
clearLotStatusesCache() debug(`Clearing cache: ${message.tableName}`)
clearLotTypesCache() clearCacheByTableName(message.tableName, false)
clearOccupancyTypesCache()
clearWorkOrderMilestoneTypesCache()
clearWorkOrderTypesCache()
}
/*
* Config Time Millis
*/
let configTimeMillis = 0
async function checkCacheIntegrity(): Promise<void> {
const timeMillis = await getConfigTableMaxTimeMillis()
if (timeMillis > configTimeMillis) {
configTimeMillis = timeMillis
clearAllCaches()
} }
} })
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" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}, },
"scripts": { "scripts": {
"start": "cross-env NODE_ENV=production node ./bin/www", "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 ./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: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", "dev:live": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:* nodemon ./bin/www.js",
"cy:open": "cypress open --config-file cypress.config.js", "cy:open": "cypress open --config-file cypress.config.js",

View File

@ -1,6 +1,6 @@
@import '@cityssm/bulma-webapp-css/cityssm'; @import '@cityssm/bulma-webapp-css/cityssm';
@import 'bulma/sass/utilities/derived-variables'; @import 'bulma/sass/utilities/derived-variables';
@import 'bulma-calendar/src/sass/index'; @import 'bulma-calendar/src/sass';
@import '@cityssm/fa-glow/fa-glow'; @import '@cityssm/fa-glow/fa-glow';
$white: #fff; $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
}