261 lines
6.9 KiB
JavaScript
261 lines
6.9 KiB
JavaScript
const os = require('os');
|
|
const fs = require('fs-extra');
|
|
const _ = require('lodash');
|
|
|
|
const WorkerState = require('./WorkerState');
|
|
const { JembaDbThread } = require('jembadb');
|
|
const DbCreator = require('./DbCreator');
|
|
const DbSearcher = require('./DbSearcher');
|
|
|
|
const ayncExit = new (require('./AsyncExit'))();
|
|
const log = new (require('./AppLogger'))().log;//singleton
|
|
const utils = require('./utils');
|
|
const genreTree = require('./genres');
|
|
|
|
//server states
|
|
const ssNormal = 'normal';
|
|
const ssDbLoading = 'db_loading';
|
|
const ssDbCreating = 'db_creating';
|
|
|
|
const stateToText = {
|
|
[ssNormal]: '',
|
|
[ssDbLoading]: 'Загрузка поисковой базы',
|
|
[ssDbCreating]: 'Создание поисковой базы',
|
|
};
|
|
|
|
//singleton
|
|
let instance = null;
|
|
|
|
class WebWorker {
|
|
constructor(config) {
|
|
if (!instance) {
|
|
this.config = config;
|
|
this.workerState = new WorkerState();
|
|
|
|
this.wState = this.workerState.getControl('server_state');
|
|
this.myState = '';
|
|
this.db = null;
|
|
this.dbSearcher = null;
|
|
|
|
ayncExit.add(this.closeDb.bind(this));
|
|
|
|
this.loadOrCreateDb();//no await
|
|
this.logServerStats();//no await
|
|
|
|
instance = this;
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
checkMyState() {
|
|
if (this.myState != ssNormal)
|
|
throw new Error('server_busy');
|
|
}
|
|
|
|
setMyState(newState, workerState = {}) {
|
|
this.myState = newState;
|
|
this.wState.set(Object.assign({}, workerState, {
|
|
state: newState,
|
|
serverMessage: stateToText[newState]
|
|
}));
|
|
}
|
|
|
|
async closeDb() {
|
|
if (this.db) {
|
|
await this.db.unlock();
|
|
this.db = null;
|
|
}
|
|
}
|
|
|
|
async createDb(dbPath) {
|
|
this.setMyState(ssDbCreating);
|
|
log('Searcher DB create start');
|
|
|
|
const config = this.config;
|
|
|
|
if (await fs.pathExists(dbPath))
|
|
throw new Error(`createDb.pathExists: ${dbPath}`);
|
|
|
|
const db = new JembaDbThread();//создаем не в потоке, чтобы лучше работал GC
|
|
await db.lock({
|
|
dbPath,
|
|
create: true,
|
|
softLock: true,
|
|
|
|
tableDefaults: {
|
|
cacheSize: 5,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const dbCreator = new DbCreator(config);
|
|
|
|
await dbCreator.run(db, (state) => {
|
|
this.setMyState(ssDbCreating, state);
|
|
|
|
if (state.fileName)
|
|
log(` load ${state.fileName}`);
|
|
if (state.recsLoaded)
|
|
log(` processed ${state.recsLoaded} records`);
|
|
if (state.job)
|
|
log(` ${state.job}`);
|
|
});
|
|
|
|
log('Searcher DB successfully created');
|
|
} finally {
|
|
await db.unlock();
|
|
}
|
|
}
|
|
|
|
async loadOrCreateDb(recreate = false) {
|
|
this.setMyState(ssDbLoading);
|
|
|
|
try {
|
|
const config = this.config;
|
|
const dbPath = `${config.dataDir}/db`;
|
|
|
|
//пересоздаем БД из INPX если нужно
|
|
if (config.recreateDb || recreate)
|
|
await fs.remove(dbPath);
|
|
|
|
if (!await fs.pathExists(dbPath)) {
|
|
await this.createDb(dbPath);
|
|
utils.freeMemory();
|
|
}
|
|
|
|
//загружаем БД
|
|
this.setMyState(ssDbLoading);
|
|
log('Searcher DB loading');
|
|
|
|
const db = new JembaDbThread();
|
|
await db.lock({
|
|
dbPath,
|
|
softLock: true,
|
|
|
|
tableDefaults: {
|
|
cacheSize: 5,
|
|
},
|
|
});
|
|
|
|
//открываем все таблицы
|
|
await db.openAll();
|
|
|
|
this.dbSearcher = new DbSearcher(config, db);
|
|
|
|
db.wwCache = {};
|
|
this.db = db;
|
|
|
|
log('Searcher DB ready');
|
|
} catch (e) {
|
|
log(LM_FATAL, e.message);
|
|
ayncExit.exit(1);
|
|
} finally {
|
|
this.setMyState(ssNormal);
|
|
}
|
|
}
|
|
|
|
async recreateDb() {
|
|
this.setMyState(ssDbCreating);
|
|
|
|
if (this.dbSearcher) {
|
|
await this.dbSearcher.close();
|
|
this.dbSearcher = null;
|
|
}
|
|
|
|
await this.closeDb();
|
|
|
|
await this.loadOrCreateDb(true);
|
|
}
|
|
|
|
async dbConfig() {
|
|
this.checkMyState();
|
|
|
|
const db = this.db;
|
|
if (!db.wwCache.config) {
|
|
const rows = await db.select({table: 'config'});
|
|
const config = {};
|
|
|
|
for (const row of rows) {
|
|
config[row.id] = row.value;
|
|
}
|
|
|
|
db.wwCache.config = config;
|
|
}
|
|
|
|
return db.wwCache.config;
|
|
}
|
|
|
|
async search(query) {
|
|
this.checkMyState();
|
|
|
|
const config = await this.dbConfig();
|
|
const result = await this.dbSearcher.search(query);
|
|
|
|
return {
|
|
author: result.result,
|
|
totalFound: result.totalFound,
|
|
inpxHash: (config.inpxHash ? config.inpxHash : ''),
|
|
};
|
|
}
|
|
|
|
async getBookList(authorId) {
|
|
this.checkMyState();
|
|
|
|
return await this.dbSearcher.getBookList(authorId);
|
|
}
|
|
|
|
async getGenreTree() {
|
|
this.checkMyState();
|
|
|
|
const config = await this.dbConfig();
|
|
|
|
let result;
|
|
const db = this.db;
|
|
if (!db.wwCache.genres) {
|
|
const genres = _.cloneDeep(genreTree);
|
|
const last = genres[genres.length - 1];
|
|
|
|
const genreValues = new Set();
|
|
for (const section of genres) {
|
|
for (const g of section.value)
|
|
genreValues.add(g.value);
|
|
}
|
|
|
|
//добавим к жанрам те, что нашлись при парсинге
|
|
const rows = await db.select({table: 'genre', map: `(r) => ({value: r.value})`});
|
|
for (const row of rows) {
|
|
if (!genreValues.has(row.value))
|
|
last.value.push({name: row.value, value: row.value});
|
|
}
|
|
|
|
result = {
|
|
genreTree: genres,
|
|
inpxHash: (config.inpxHash ? config.inpxHash : ''),
|
|
};
|
|
|
|
db.wwCache.genres = result;
|
|
} else {
|
|
result = db.wwCache.genres;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async logServerStats() {
|
|
while (1) {// eslint-disable-line
|
|
try {
|
|
const memUsage = process.memoryUsage().rss/(1024*1024);//Mb
|
|
let loadAvg = os.loadavg();
|
|
loadAvg = loadAvg.map(v => v.toFixed(2));
|
|
|
|
log(`Server info [ memUsage: ${memUsage.toFixed(2)}MB, loadAvg: (${loadAvg.join(', ')}) ]`);
|
|
} catch (e) {
|
|
log(LM_ERR, e.message);
|
|
}
|
|
await utils.sleep(5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = WebWorker; |