Иногда, например, при обработке больших массивов данных, для использования максимума ресурсов окружения и сокращения общего затраченного времени работы, нам приходится использовать конкурирующие процессы, которые одновременно выполняют однотипные задачи над разными объектами.

Предположим, мы разрабатываем простой пакет для npm. Назовём его, например, storage (хранилище). Заранее предусмотрим возможность использования одного из нескольких типов хранилищ, например, FsStorage (файловое хранилище), MysqlStorage (MySQL-хранилище), MongoStorage (Mongo-хранилище).

Накидаем содержимое исходных кодов нашего пакета (под спойлером):

Примерный набросок исходного кода проекта
  • package.json:

    {
        "name": "storage",
        "version": "0.1.0",
        "main": "./lib/index.js",
        "dependencies": {
            "mysql": "*",
            "mongoose": "*"
        }
    }
  • lib/index.js:

    module.exports = {
        FsStorage: require('./fs-storage.jsx'),
        MysqlStorage: require('./mysql-storage.jsx'),
        MongoStorage: require('./mongo-storage.jsx')
    };
    
  • lib/fs-storage.js:

    var fs = require('fs');
    
    module.exports = FsStorage;
    
    function FsStorage() {
        // init code...
    }
    
  • lib/mysql-storage.js:

    var mysql = require('mysql');
    
    module.exports = MysqlStorage;
    
    function MysqlStorage() {
        // init code...
    }
    
  • lib/mongo-storage.js:

    var mongoose = require('mongoose');
    
    module.exports = MongoStorage;
    
    function MongoStorage() {
        // init code...
    }
    


Непосредственный код зависимостей mysql и mongoose для демонстрации нам необязателен. Поэтому, разместим вместо кода mysql и mongoose заглушки (под спойлером):

Исходный код заглушек модулей mysql и mongoose
  • node_modules/mysql/index.js:
    console.log('MySQL module loaded');
    
  • node_modules/mongoose/index.js:
    console.log('Mongoose module loaded');
    

Тогда файловая структура пакета будет выглядеть следующим образом (под спойлером):

Макет дерева файловой структуры
storage/
    ├ lib/
    │    ├ index.js
    │    ├ fs-storage.js
    │    ├ mongo-storage.js
    │    └ mysql-storage.js
    ├ node_modules/
    │    ├ mongoose/
    │    │    └ index.js
    │    └ mysql/
    │         └ index.js
    └ package.json

Теперь представим, что у нас есть мега-большая задача, которую будем выполнять с помощью нескольких дочерних процессов, и по ходу выполнения нам необходимо использовать хранилище в виде файлов.

var storage = require('storage');
var fsStorage = new storage.FsStorage();

Запускаем и наблюдаем: каждый дочерний процесс занимает памяти на порядок больше, чем ожидалось. А если количество конкурирующих процессов перевалит за сотню, и это не единственная задача, которая выполняется на сервере в реальном времени?!

Тут мы и понимаем, что, например, при использовании файлового хранилища, незачем загружать как библиотеку по управлению базами данных MySQL, так и ODM-клиент Mongo.

Сразу после вызова require('storage') в консоль выводятся сообщения:

MySQL module loaded
Mongoose module loaded

Поэкспериментировав с методом Object.defineProperty(), я добился удивительного результата, который оформил в виде функции demandProperty():

function demandProperty(obj, name, modPath) {
    Object.defineProperty(obj, name, {
        configurable: true,
        enumerable: true,
        get: function() {
            var mod = require(modPath);
            Object.defineProperty(obj, name, {
                configurable: false,
                enumerable: true,
                value: mod
            });
            return mod
        }
    })
}

Принцип работы прост: Вместо прямой ссылки, например, на MysqlStorage(), создается акцессор (геттер). При любом запросе к акцессору, срабатывает require(), а сам акцессор возвращает результат require(). Кроме того, с помощью того же Object.defineProperty() мы устанавливаем обратно прямую ссылку на тот же результат require() вместо акцессора (то есть, на MysqlStorage()). Так все запросы (разумеется, кроме первого) будут работать с той же скоростью и надежностью от утечек, как если бы мы оставили классический require().

Изменим lib/index.js. Заменим:

module.exports = {
    FsStorage: require('./fs-storage.jsx'),
    MysqlStorage: require('./mysql-storage.jsx'),
    MongoStorage: require('./mongo-storage.jsx'),
};
на:

demandProperty(module.exports, 'FsStorage', './fs-storage.jsx');
    demandProperty(module.exports, 'MysqlStorage', './mysql-storage.jsx');
    demandProperty(module.exports, 'MongoStorage', './mongo-storage.jsx');

И используем:

var storage = require('storage');

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Getter],
    MysqlStorage: [Getter],
    MongoStorage: [Getter] }
*/

console.log(util.inspect(storage.FsStorage.name));
// =>  'FsStorage'

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Function: FsStorage],
    MysqlStorage: [Getter],
    MongoStorage: [Getter] }
*/

var mysql = new storage.MysqlStorage();
// =>  MySQL module loaded
console.log(util.inspect(mysql));
// =>  '{}'

console.log(util.inspect(storage));
/* =>
    { FsStorage: [Function: FsStorage],
    MysqlStorage: [Function: MysqlStorage],
    MongoStorage: [Getter] }
*/

Есть ещё одна тонкость. Если определение функции demandProperty() вынести за пределы модулей в папке lib, последним аргументом необходимо передавать полный путь до модуля, иначе require() будет искать модуль в той папке, где определен demandProperty():

demandProperty(module.exports, 'FsStorage', path.resolve(__dirname, './fs-storage.jsx'));
demandProperty(module.exports, 'MysqlStorage', path.resolve(__dirname, './mysql-storage.jsx'));
demandProperty(module.exports, 'MongoStorage', path.resolve(__dirname, './mongo-storage.jsx'));


Всем удачных экспериментов!

Комментарии (20)


  1. Jabher
    01.06.2015 22:38

    Lazy-load штука хорошая. Но я бы вам рекомендовал посмотреть на асинхронные задачи, все-таки в самой ноде обычно проводится на порядок меньше времени, чем в libuv.
    К тому же клиент обычно не сразу инициализируется, а времени на загрузку тратится совсем немного.

    У меня сейчас примерно так работает одна система, которая весьма бодро аггрегирует данные из соцсетей.

    //noinspection JSUnresolvedVariable
    var remaining_thread_count = Number(process.env.THREAD_COUNT) || 150;
    
    export async function init() {
        ...миграции БД...
        while (remaining_thread_count--)
        /*important! not an async call, that's an fibering*/
            start_thread();
    }
    
    async function start_thread() {
        await (await getNextTask());
        start_thread();
    }
    


    1. hell0w0rd
      01.06.2015 23:06

      await (await getNextTask());
      

      оО
      await ожидает промис, а промис не может зарезолвить промис, таким образом что делает этот код?


      1. Jabher
        01.06.2015 23:48

        что делает этот код
        работает

        async-функции МОГУТ возвращать промисы как результат работы.


        1. hell0w0rd
          02.06.2015 00:29

          Интересно. Не является ли это специфичной для async/await «фичей», или багом regenerator?

          function test() {
            return new Promise((resolve) => {
              resolve(Promise.resolve(5));
            });
          }
          
          test().then((val) => console.log(val)) // 5
          


        1. hell0w0rd
          02.06.2015 00:34

          таки нет, вы не правы.
          Этот код вполне рабочий:

          (async function start_thread() {
            const result = await getNextTask();
            console.log(`got ${result} from promise`);
          })();
          
          async function getNextTask(){
            return new Promise(resolve => resolve(5))
          }
          

          // этот код эквивалентен
          log(await (await foo()));
          // этому
          foo().then(r => r).then(r => log(r));
          

          И да, все async-функции возвращают промис, как результат работы, by design.


          1. Jabher
            02.06.2015 02:25

            я имел ввиду, что await getPromise() способен асинхронно вернуть промис.
            То, что с точки зрения архитектуры они все построены поверх промисов — я знаю.


            1. hell0w0rd
              02.06.2015 12:34

              Так зачем вы используете конструкцию await (await ...);?


    1. ckr Автор
      02.06.2015 06:56

      По началу, я тоже использовал одновременное асинхронное выполнение кода в несколько потоков. Но, код выполняется в одном процессе, соответственно, на одном ядре CPU. Для использования максимума ресурсов обычно используется оба подхода: код работает в несколько процессов, каждый процесс из которых выполняет код в несколько потоков.

      Повышать количество потоков в каждом процессе может быть выгодно для экономии оперативной памяти, т.к. модули для второго и последующих потоков уже загруженны. Главное — не переборщить, во избежание зависания процессов. Но использование нескольких процессов тоже неизбежно.


      1. ckr Автор
        02.06.2015 07:40

        Кроме того, процессы node могут иметь такие состояния, когда их необходимо завершить или перезапустить:

        1. idle: процесс использует CPU на 0% продолжительное время. Тут может быть два варианта: если процесс выполнял какую-то работу, скорее всего процесс по какой-то причине потерял callback или что-то в этом роде. Такой процесс необходимо перезагрузить. Если данный процесс реализует, например, http-сервер с каким-нибудь малопосещаемым сайтом, то скорее всего такое состояние обусловленно тем, что на сайт никто давно не заходил. Такой процесс можно завершить, а при запросе на сайт, снова по быстрому поднять http-сервер.
        2. CPU-limit: процесс использует CPU на 100% (или свой ограниченный максимум) продолжительное время. Такой процесс скорее всего зациклился или попал на невыполнимый участок задачи. Такой процесс можно перезагрузить, чтобы сам не застревал на одной задаче и не мешал другим конкурирующим процессам.
        3. RAM-limit: процесс использует максимум выделенной для него памяти. Не секрет, что node-процессы по ходу своего выполнения текут. Совсем ожиревшие процессы тоже можно перезагружать. Это напоминает борьбу с утечками методом PHP. — То есть, зачем удалять ненужные переменные?! — все переменные удаляются вместе с завершением процесса.


  1. MarcusAurelius
    02.06.2015 02:49
    +2

    Без Object.defineProperty эта задача прекрасно решалась, просто библиотека вместо хеша конструкторов должна возвращать функцию с одним параметром — именем провайдера. Это можно назвать фабрикой конструкторов. Фабрика делает нужный require и выглядит это гораздо лаконичнее.

    Реализация модуля storage:

    module.exports = function(providerName) {
      return require('./'+providerName+'-storage.jsx');
    };
    

    Вместо этого:
    var storage = require('storage');
    var fsStorage = new storage.FsStorage();
    

    Загрузка модуля с провайдером выглядит так:
    var storageConstructor = require('storage')('fs'),
        fsStorage = new storageConstructor();
    


    1. ckr Автор
      02.06.2015 06:46

      Вы правы, можно обойтись и без Object.defineProperty().
      Правда, я не использую в проектах фабрики из-за ненаглядности структуры кода, например, для IDE.

      Так же мы используем общую документацию по всем проектам, пакетам, модулям. Для представления списка всех классов в древовидной форме, нам приходится лишь помогать jsdoc лишь указывая что есть @constructor, а что @namespace. Использование фабрик требовало бы модернизации сборщика/генератора документации.


      1. MarcusAurelius
        02.06.2015 10:59

        А что, jsdoc хорошо понимает добавление геттеров через Object.defineProperty?


        1. ckr Автор
          02.06.2015 11:13

          Нет, как раз за это отвечает метки @constructor, @namespace и остальные.

          Например, для обозначения namespace я добавлю в index.js:

          /** @namespace storage */
          


          А для класса, например:
          /**
            * @name storage.FsStorage
            * @extends storage.BaseStorage 
            * @constructor 
            */
          


          Классы я обычно определяю олдскул-способом Function().prototype.

          Так, jsdoc шарится по всем файлам исходных кодов, а IDE тоже прекрасно понимает такие комментарии, структура кода всегда под рукой. Код готов к любому рефакторингу.


          1. MarcusAurelius
            02.06.2015 11:19

            Вообще, меня jsdoc и IDE (все) очень огорчают. JavaScript не мыслим без примесей, замыканий и фабрик, а я вообще не могу жить без динамических примесей в прототипы. Они позволяют мне писать в 10 раз меньше кода. Пока не удалось найти систему комментирования исходников, генерации доков, или даже систему автодополнения кода для IDE, которая бы все это могла адекватно отпарсить и уразуметь. Ее конечно можно написать, но что-то подсказывает мне, что она будет очень. Обратное решение, когда все нужно декларировать явно, потребует много писанины в комментариях. Так что, единственный приличный способ сделать это, по моему мнению, это чтобы IDE подключались через дебагер к процессам и показывали содержимое прямо из памяти.


            1. ckr Автор
              02.06.2015 12:06

              Я не понимаю вашего негодования.

              Я не против фабрик, но у каждого класса должно быть «жесткое» имя, закрепленное в своем namespace.
              То есть, да, разумно к тому коду, что есть приделать возможность получения класса через фабрику. Это бы помогло в случае, например, если тип хранилища указан в конфиге. Но обязательно должна остаться возможность получить класс и по «жесткой» ссылке.

              /**
               * @name storage.getStorage
               * @returns {storage.FsStorage|storage.MysqlStorage|storage.MongoStorage}
               */
              


              Замыкания к процессу документирования вообще никакой гранью не могу отнести.

              По поводу динамических примесей в прототипы. На Node.JS это реализованно util.inherits().
              А в jsdoc множественное наследование можно описать:

              /**
               * @name storage.MysqlStorage
               * @extends {storage.BaseStorage|storage.SqlStorage}
               * @constructor 
               */
              


              1. MarcusAurelius
                02.06.2015 13:02

                Где негодование? Нет признаков негодования.

                Зачем нужна возможность именования классов по жесткой ссылке?
                Замыкания относятся к документированию потому, что это не только способ написания прикладного кода, а еще и способ непрямого наследования. При использовании замыканий наследуются и переопределяются не классы и прототипы, а функции. Например каррирование — это функциональное наследование через замыкания. Например:

                function curry(x) {
                    return function(y) {
                        return x + y;
                    }
                }
                

                Можем теперь делать: или сразу var res = curry(4)(5); или получать унаследованную функцию var fn = curry(4); и потом var var res = fn(5); С первого взгляда, может быть непонятно, зачем наследовать функции, но таким способом можно реализовывать метапрограммирование (порождающее программирование), см. примеры: habrahabr.ru/post/227753 Там замыкания повсеместно для порождения модификаций используются, и как это скормить в jsdoc? Это же все в рантайме создается.

                Теперь про util.inherits(); Это способ классового наследования. Это не имеет ни какого отношения к примесям в прототипы. Примеси в прототипы нужны когда мы хотим, опять же, динамически создать прототип для массового порождения объектов. Например, имеем Client.prototype, и заранее мы не знаем, будет включен или отключен модуль безопасности. Когда он включен, то делается require('security')(Client); И уже сам модуль безопасности примешивает к Client.prototype свои методы, например: Client.prototype.signIn(login, password); и перекрывает своими методами часть базовых, Client же не знал до этого что есть методы модуля безопасности, и он должен так расширить Client, чтобы новые методы объявить и вызывать их из уже имеющихся.


                1. ckr Автор
                  02.06.2015 13:59
                  +1

                  По части curr вы правы. Для корректного понимания IDE можно добавить в комментарий @returns {function}. Ну, а как пользоваться сей конструкцией можно разъяснить в свободной форме в формате markdown в том же комментарии.

                  По поводу примисей и наследования я понял, что Вы хотели донести. Но все это можно реализовать и с util.inherits(). Разница лишь в том, что util.inherits(Client, Security) оставит методы и свойства Client, если таковые имеются в Security.

                  То есть, например, есть классы Security и Client. Если реализовать так, то методы и свойства Security будут поверх Client:

                  var SecurityClient = mixClass(Client, Security);
                  
                  function mixClass(cl, mx) {
                      var resClass = function(){};
                      util.inherits(resClass, mx);
                      util.inherits(resClass, cl);
                      return resClass;
                  }
                  


                  Как оформить сие добро в jsdoc не могу уверенно предложить. Но, могу с уверенностью сказать, может примиси и удобный для web инструмент (в браузере много стандартных классов, в которых можно расширять функционал: HTMLDocument, XMLHttpRequest), но в Node.JS я бы уверенно обходился бы без них, с базовым одноуровневым наследованием.

                  Так или иначе, все зависит от конкретной задачи.


                  1. ckr Автор
                    02.06.2015 14:12

                    Заметил еще косяк, что потерял конструктор. Правильнее было бы так:

                    var SecurityClient = mixClass(Client, Security);
                    
                    function mixClass(cl, mx) {
                        var resClass = cl.prototype.constructor;
                        util.inherits(resClass, mx);
                        util.inherits(resClass, cl);
                        return resClass;
                    }
                    


                    1. ckr Автор
                      02.06.2015 14:32

                      Даже возвращать новый класс необязательно, можно вкорячить прототип и в старый класс.

                      mixClass(Client, Security);
                      
                      function mixClass(cl, mx) {
                          var resClass = cl.prototype.constructor;
                          util.inherits(resClass, mx);
                          util.inherits(resClass, cl);
                          cl.prototype = Object.create(resClass.prototype);
                      }
                      


  1. MarcusAurelius
    02.06.2015 10:59

    Не использовать фабрики в js очень большие жертвы. Кстати, можно даже сделать фабрику фабрик, таким образом экранировать класс провайдера от модификации и избавиться от new.