WebWorker.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. const os = require('os');
  2. const fs = require('fs-extra');
  3. const _ = require('lodash');
  4. const WorkerState = require('./WorkerState');
  5. const { JembaDbThread } = require('jembadb');
  6. const DbCreator = require('./DbCreator');
  7. const DbSearcher = require('./DbSearcher');
  8. const ayncExit = new (require('./AsyncExit'))();
  9. const log = new (require('./AppLogger'))().log;//singleton
  10. const utils = require('./utils');
  11. const genreTree = require('./genres');
  12. //server states
  13. const ssNormal = 'normal';
  14. const ssDbLoading = 'db_loading';
  15. const ssDbCreating = 'db_creating';
  16. const stateToText = {
  17. [ssNormal]: '',
  18. [ssDbLoading]: 'Загрузка поисковой базы',
  19. [ssDbCreating]: 'Создание поисковой базы',
  20. };
  21. //singleton
  22. let instance = null;
  23. class WebWorker {
  24. constructor(config) {
  25. if (!instance) {
  26. this.config = config;
  27. this.workerState = new WorkerState();
  28. this.wState = this.workerState.getControl('server_state');
  29. this.myState = '';
  30. this.db = null;
  31. this.dbSearcher = null;
  32. ayncExit.add(this.closeDb.bind(this));
  33. this.loadOrCreateDb();//no await
  34. this.logServerStats();//no await
  35. instance = this;
  36. }
  37. return instance;
  38. }
  39. checkMyState() {
  40. if (this.myState != ssNormal)
  41. throw new Error('server_busy');
  42. }
  43. setMyState(newState, workerState = {}) {
  44. this.myState = newState;
  45. this.wState.set(Object.assign({}, workerState, {
  46. state: newState,
  47. serverMessage: stateToText[newState]
  48. }));
  49. }
  50. async closeDb() {
  51. if (this.db) {
  52. await this.db.unlock();
  53. this.db = null;
  54. }
  55. }
  56. async createDb(dbPath) {
  57. this.setMyState(ssDbCreating);
  58. log('Searcher DB create start');
  59. const config = this.config;
  60. if (await fs.pathExists(dbPath))
  61. throw new Error(`createDb.pathExists: ${dbPath}`);
  62. const db = new JembaDbThread();//создаем не в потоке, чтобы лучше работал GC
  63. await db.lock({
  64. dbPath,
  65. create: true,
  66. softLock: true,
  67. tableDefaults: {
  68. cacheSize: 5,
  69. },
  70. });
  71. try {
  72. const dbCreator = new DbCreator(config);
  73. await dbCreator.run(db, (state) => {
  74. this.setMyState(ssDbCreating, state);
  75. if (state.fileName)
  76. log(` load ${state.fileName}`);
  77. if (state.recsLoaded)
  78. log(` processed ${state.recsLoaded} records`);
  79. if (state.job)
  80. log(` ${state.job}`);
  81. });
  82. log('Searcher DB successfully created');
  83. } finally {
  84. await db.unlock();
  85. }
  86. }
  87. async loadOrCreateDb(recreate = false) {
  88. this.setMyState(ssDbLoading);
  89. try {
  90. const config = this.config;
  91. const dbPath = `${config.dataDir}/db`;
  92. //пересоздаем БД из INPX если нужно
  93. if (config.recreateDb || recreate)
  94. await fs.remove(dbPath);
  95. if (!await fs.pathExists(dbPath)) {
  96. await this.createDb(dbPath);
  97. utils.freeMemory();
  98. }
  99. //загружаем БД
  100. this.setMyState(ssDbLoading);
  101. log('Searcher DB loading');
  102. const db = new JembaDbThread();
  103. await db.lock({
  104. dbPath,
  105. softLock: true,
  106. tableDefaults: {
  107. cacheSize: 5,
  108. },
  109. });
  110. //открываем все таблицы
  111. await db.openAll();
  112. this.dbSearcher = new DbSearcher(config, db);
  113. db.wwCache = {};
  114. this.db = db;
  115. log('Searcher DB ready');
  116. } catch (e) {
  117. log(LM_FATAL, e.message);
  118. ayncExit.exit(1);
  119. } finally {
  120. this.setMyState(ssNormal);
  121. }
  122. }
  123. async recreateDb() {
  124. this.setMyState(ssDbCreating);
  125. if (this.dbSearcher) {
  126. await this.dbSearcher.close();
  127. this.dbSearcher = null;
  128. }
  129. await this.closeDb();
  130. await this.loadOrCreateDb(true);
  131. }
  132. async dbConfig() {
  133. this.checkMyState();
  134. const db = this.db;
  135. if (!db.wwCache.config) {
  136. const rows = await db.select({table: 'config'});
  137. const config = {};
  138. for (const row of rows) {
  139. config[row.id] = row.value;
  140. }
  141. db.wwCache.config = config;
  142. }
  143. return db.wwCache.config;
  144. }
  145. async search(query) {
  146. this.checkMyState();
  147. const config = await this.dbConfig();
  148. const result = await this.dbSearcher.search(query);
  149. return {
  150. author: result.result,
  151. totalFound: result.totalFound,
  152. inpxHash: (config.inpxHash ? config.inpxHash : ''),
  153. };
  154. }
  155. async getBookList(authorId) {
  156. this.checkMyState();
  157. return await this.dbSearcher.getBookList(authorId);
  158. }
  159. async getGenreTree() {
  160. this.checkMyState();
  161. const config = await this.dbConfig();
  162. let result;
  163. const db = this.db;
  164. if (!db.wwCache.genres) {
  165. const genres = _.cloneDeep(genreTree);
  166. const last = genres[genres.length - 1];
  167. const genreValues = new Set();
  168. for (const section of genres) {
  169. for (const g of section.value)
  170. genreValues.add(g.value);
  171. }
  172. //добавим к жанрам те, что нашлись при парсинге
  173. const genreParsed = new Set();
  174. const rows = await db.select({table: 'genre', map: `(r) => ({value: r.value})`});
  175. for (const row of rows) {
  176. genreParsed.add(row.value);
  177. if (!genreValues.has(row.value))
  178. last.value.push({name: row.value, value: row.value});
  179. }
  180. //уберем те, которые не нашлись при парсинге
  181. for (let j = 0; j < genres.length; j++) {
  182. const section = genres[j];
  183. for (let i = 0; i < section.value.length; i++) {
  184. const g = section.value[i];
  185. if (!genreParsed.has(g.value))
  186. section.value.splice(i--, 1);
  187. }
  188. if (!section.value.length)
  189. genres.splice(j--, 1);
  190. }
  191. result = {
  192. genreTree: genres,
  193. inpxHash: (config.inpxHash ? config.inpxHash : ''),
  194. };
  195. db.wwCache.genres = result;
  196. } else {
  197. result = db.wwCache.genres;
  198. }
  199. return result;
  200. }
  201. async logServerStats() {
  202. while (1) {// eslint-disable-line
  203. try {
  204. const memUsage = process.memoryUsage().rss/(1024*1024);//Mb
  205. let loadAvg = os.loadavg();
  206. loadAvg = loadAvg.map(v => v.toFixed(2));
  207. log(`Server info [ memUsage: ${memUsage.toFixed(2)}MB, loadAvg: (${loadAvg.join(', ')}) ]`);
  208. } catch (e) {
  209. log(LM_ERR, e.message);
  210. }
  211. await utils.sleep(5000);
  212. }
  213. }
  214. }
  215. module.exports = WebWorker;