repair race condition in tests + improve docs

pull/11/head
Timothée Rebours 3 years ago
parent 8b776b5c45
commit dbcc3647d1
  1. 109
      API.md
  2. 3
      lib/cursor.js
  3. 104
      lib/datastore.js
  4. 6
      lib/storage.js
  5. 34
      test/persistence.async.test.js
  6. 2
      test/persistence.test.js
  7. 138
      test_lac/loadAndCrash.test.js
  8. 108
      test_lac/openFds.test.js

109
API.md

@ -258,14 +258,15 @@ Will return pointers to matched elements (shallow copies), returning full copies
* [.ttlIndexes](#Datastore+ttlIndexes) : <code>Object.&lt;string, number&gt;</code> * [.ttlIndexes](#Datastore+ttlIndexes) : <code>Object.&lt;string, number&gt;</code>
* [.autoloadPromise](#Datastore+autoloadPromise) : <code>Promise</code> * [.autoloadPromise](#Datastore+autoloadPromise) : <code>Promise</code>
* [.compareStrings()](#Datastore+compareStrings) : [<code>compareStrings</code>](#compareStrings) * [.compareStrings()](#Datastore+compareStrings) : [<code>compareStrings</code>](#compareStrings)
* [.loadDatabase(callback)](#Datastore+loadDatabase) * [.loadDatabase([callback])](#Datastore+loadDatabase)
* [.loadDatabaseAsync()](#Datastore+loadDatabaseAsync) ⇒ <code>Promise</code> * [.loadDatabaseAsync()](#Datastore+loadDatabaseAsync) ⇒ <code>Promise</code>
* [.getAllData()](#Datastore+getAllData) ⇒ [<code>Array.&lt;document&gt;</code>](#document) * [.getAllData()](#Datastore+getAllData) ⇒ [<code>Array.&lt;document&gt;</code>](#document)
* [.ensureIndex(options, callback)](#Datastore+ensureIndex) * [.ensureIndex(options, [callback])](#Datastore+ensureIndex)
* [.ensureIndexAsync(options)](#Datastore+ensureIndexAsync) ⇒ <code>Promise.&lt;void&gt;</code> * [.ensureIndexAsync(options)](#Datastore+ensureIndexAsync) ⇒ <code>Promise.&lt;void&gt;</code>
* [.removeIndex(fieldName, callback)](#Datastore+removeIndex) * [.removeIndex(fieldName, callback)](#Datastore+removeIndex)
* [.removeIndexAsync(fieldName)](#Datastore+removeIndexAsync) ⇒ <code>Promise.&lt;void&gt;</code> * [.removeIndexAsync(fieldName)](#Datastore+removeIndexAsync) ⇒ <code>Promise.&lt;void&gt;</code>
* [.insertAsync(newDoc)](#Datastore+insertAsync) ⇒ [<code>Promise.&lt;document&gt;</code>](#document) * [.insert(newDoc, [callback])](#Datastore+insert)
* [.insertAsync(newDoc)](#Datastore+insertAsync) ⇒ <code>Promise.&lt;(document\|Array.&lt;document&gt;)&gt;</code>
* [.count(query, [callback])](#Datastore+count) ⇒ <code>Cursor.&lt;number&gt;</code> \| <code>undefined</code> * [.count(query, [callback])](#Datastore+count) ⇒ <code>Cursor.&lt;number&gt;</code> \| <code>undefined</code>
* [.countAsync(query)](#Datastore+countAsync) ⇒ <code>Cursor.&lt;number&gt;</code> * [.countAsync(query)](#Datastore+countAsync) ⇒ <code>Cursor.&lt;number&gt;</code>
* [.find(query, [projection], [callback])](#Datastore+find) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code> \| <code>undefined</code> * [.find(query, [projection], [callback])](#Datastore+find) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code> \| <code>undefined</code>
@ -408,21 +409,21 @@ letters. Native <code>localCompare</code> will most of the time be the right cho
**Access**: protected **Access**: protected
<a name="Datastore+loadDatabase"></a> <a name="Datastore+loadDatabase"></a>
### neDB.loadDatabase(callback) ### neDB.loadDatabase([callback])
<p>Load the database from the datafile, and trigger the execution of buffered commands if any.</p> <p>Callback version of [loadDatabaseAsync](#Datastore+loadDatabaseAsync).</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#loadDatabaseAsync
**Params** **Params**
- callback [<code>NoParamCallback</code>](#NoParamCallback) - [callback] [<code>NoParamCallback</code>](#NoParamCallback)
<a name="Datastore+loadDatabaseAsync"></a> <a name="Datastore+loadDatabaseAsync"></a>
### neDB.loadDatabaseAsync() ⇒ <code>Promise</code> ### neDB.loadDatabaseAsync() ⇒ <code>Promise</code>
<p>Async version of [loadDatabase](#Datastore+loadDatabase).</p> <p>Load the database from the datafile, and trigger the execution of buffered commands if any.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#loadDatabase
<a name="Datastore+getAllData"></a> <a name="Datastore+getAllData"></a>
### neDB.getAllData() ⇒ [<code>Array.&lt;document&gt;</code>](#document) ### neDB.getAllData() ⇒ [<code>Array.&lt;document&gt;</code>](#document)
@ -431,36 +432,40 @@ letters. Native <code>localCompare</code> will most of the time be the right cho
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
<a name="Datastore+ensureIndex"></a> <a name="Datastore+ensureIndex"></a>
### neDB.ensureIndex(options, callback) ### neDB.ensureIndex(options, [callback])
<p>Ensure an index is kept for this field. Same parameters as lib/indexes <p>Callback version of [ensureIndex](#Datastore+ensureIndex).</p>
This function acts synchronously on the indexes, however the persistence of the indexes is deferred with the
executor.
Previous versions said explicitly the callback was optional, it is now recommended setting one.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#ensureIndex
**Params** **Params**
- options <code>object</code> - options <code>object</code>
- .fieldName <code>string</code> - <p>Name of the field to index. Use the dot notation to index a field in a nested document.</p> - .fieldName <code>string</code>
- [.unique] <code>boolean</code> <code> = false</code> - <p>Enforce field uniqueness. Note that a unique index will raise an error if you try to index two documents for which the field is not defined.</p> - [.unique] <code>boolean</code> <code> = false</code>
- [.sparse] <code>boolean</code> <code> = false</code> - <p>don't index documents for which the field is not defined. Use this option along with &quot;unique&quot; if you want to accept multiple documents for which it is not defined.</p> - [.sparse] <code>boolean</code> <code> = false</code>
- [.expireAfterSeconds] <code>number</code> - <p>if set, the created index is a TTL (time to live) index, that will automatically remove documents when the system date becomes larger than the date on the indexed field plus <code>expireAfterSeconds</code>. Documents where the indexed field is not specified or not a <code>Date</code> object are ignored</p> - [.expireAfterSeconds] <code>number</code>
- callback [<code>NoParamCallback</code>](#NoParamCallback) - <p>Callback, signature: err</p> - [callback] [<code>NoParamCallback</code>](#NoParamCallback)
<a name="Datastore+ensureIndexAsync"></a> <a name="Datastore+ensureIndexAsync"></a>
### neDB.ensureIndexAsync(options) ⇒ <code>Promise.&lt;void&gt;</code> ### neDB.ensureIndexAsync(options) ⇒ <code>Promise.&lt;void&gt;</code>
<p>Async version of [ensureIndex](#Datastore+ensureIndex).</p> <p>Ensure an index is kept for this field. Same parameters as lib/indexes
This function acts synchronously on the indexes, however the persistence of the indexes is deferred with the
executor.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#ensureIndex
**Params** **Params**
- options <code>object</code> - options <code>object</code>
- .fieldName <code>string</code> - <p>Name of the field to index. Use the dot notation to index a field in a nested document.</p> - .fieldName <code>string</code> - <p>Name of the field to index. Use the dot notation to index a field in a nested
- [.unique] <code>boolean</code> <code> = false</code> - <p>Enforce field uniqueness. Note that a unique index will raise an error if you try to index two documents for which the field is not defined.</p> document.</p>
- [.sparse] <code>boolean</code> <code> = false</code> - <p>Don't index documents for which the field is not defined. Use this option along with &quot;unique&quot; if you want to accept multiple documents for which it is not defined.</p> - [.unique] <code>boolean</code> <code> = false</code> - <p>Enforce field uniqueness. Note that a unique index will raise an error
- [.expireAfterSeconds] <code>number</code> - <p>If set, the created index is a TTL (time to live) index, that will automatically remove documents when the system date becomes larger than the date on the indexed field plus <code>expireAfterSeconds</code>. Documents where the indexed field is not specified or not a <code>Date</code> object are ignored</p> if you try to index two documents for which the field is not defined.</p>
- [.sparse] <code>boolean</code> <code> = false</code> - <p>Don't index documents for which the field is not defined. Use this option
along with &quot;unique&quot; if you want to accept multiple documents for which it is not defined.</p>
- [.expireAfterSeconds] <code>number</code> - <p>If set, the created index is a TTL (time to live) index, that will
automatically remove documents when the system date becomes larger than the date on the indexed field plus
<code>expireAfterSeconds</code>. Documents where the indexed field is not specified or not a <code>Date</code> object are ignored.</p>
<a name="Datastore+removeIndex"></a> <a name="Datastore+removeIndex"></a>
@ -487,31 +492,45 @@ field in a nested document.</p>
- fieldName <code>string</code> - <p>Field name of the index to remove. Use the dot notation to remove an index referring to a - fieldName <code>string</code> - <p>Field name of the index to remove. Use the dot notation to remove an index referring to a
field in a nested document.</p> field in a nested document.</p>
<a name="Datastore+insertAsync"></a> <a name="Datastore+insert"></a>
### neDB.insertAsync(newDoc) ⇒ [<code>Promise.&lt;document&gt;</code>](#document) ### neDB.insert(newDoc, [callback])
<p>Async version of [Datastore#insert](Datastore#insert).</p> <p>Callback version of [insertAsync](#Datastore+insertAsync).</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#insertAsync
**Params** **Params**
- newDoc [<code>document</code>](#document) | [<code>Array.&lt;document&gt;</code>](#document) - newDoc [<code>document</code>](#document) | [<code>Array.&lt;document&gt;</code>](#document)
- [callback] [<code>SingleDocumentCallback</code>](#SingleDocumentCallback) | [<code>MultipleDocumentsCallback</code>](#MultipleDocumentsCallback)
<a name="Datastore+insertAsync"></a>
### neDB.insertAsync(newDoc) ⇒ <code>Promise.&lt;(document\|Array.&lt;document&gt;)&gt;</code>
<p>Insert a new document, or new documents.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore)
**Returns**: <code>Promise.&lt;(document\|Array.&lt;document&gt;)&gt;</code> - <p>The document(s) inserted.</p>
**Params**
- newDoc [<code>document</code>](#document) | [<code>Array.&lt;document&gt;</code>](#document) - <p>Document or array of documents to insert.</p>
<a name="Datastore+count"></a> <a name="Datastore+count"></a>
### neDB.count(query, [callback]) ⇒ <code>Cursor.&lt;number&gt;</code> \| <code>undefined</code> ### neDB.count(query, [callback]) ⇒ <code>Cursor.&lt;number&gt;</code> \| <code>undefined</code>
<p>Count all documents matching the query.</p> <p>Callback-version of [countAsync](#Datastore+countAsync).</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#countAsync
**Params** **Params**
- query [<code>query</code>](#query) - <p>MongoDB-style query</p> - query [<code>query</code>](#query)
- [callback] [<code>countCallback</code>](#Datastore..countCallback) - <p>If given, the function will return undefined, otherwise it will return the Cursor.</p> - [callback] [<code>countCallback</code>](#Datastore..countCallback)
<a name="Datastore+countAsync"></a> <a name="Datastore+countAsync"></a>
### neDB.countAsync(query) ⇒ <code>Cursor.&lt;number&gt;</code> ### neDB.countAsync(query) ⇒ <code>Cursor.&lt;number&gt;</code>
<p>Async version of [count](#Datastore+count).</p> <p>Count all documents matching the query.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**Returns**: <code>Cursor.&lt;number&gt;</code> - <p>count</p> **Returns**: <code>Cursor.&lt;number&gt;</code> - <p>count</p>
@ -522,21 +541,22 @@ field in a nested document.</p>
<a name="Datastore+find"></a> <a name="Datastore+find"></a>
### neDB.find(query, [projection], [callback]) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code> \| <code>undefined</code> ### neDB.find(query, [projection], [callback]) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code> \| <code>undefined</code>
<p>Find all documents matching the query <p>Callback version of [findAsync](#Datastore+findAsync).</p>
If no callback is passed, we return the cursor so that user can limit, skip and finally exec</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#findAsync
**Params** **Params**
- query [<code>query</code>](#query) - <p>MongoDB-style query</p> - query [<code>query</code>](#query)
- [projection] [<code>projection</code>](#projection) | [<code>MultipleDocumentsCallback</code>](#MultipleDocumentsCallback) <code> = {}</code> - <p>MongoDB-style projection. If not given, will be - [projection] [<code>projection</code>](#projection) | [<code>MultipleDocumentsCallback</code>](#MultipleDocumentsCallback) <code> = {}</code>
interpreted as the callback.</p> - [callback] [<code>MultipleDocumentsCallback</code>](#MultipleDocumentsCallback)
- [callback] [<code>MultipleDocumentsCallback</code>](#MultipleDocumentsCallback) - <p>Optional callback, signature: err, docs</p>
<a name="Datastore+findAsync"></a> <a name="Datastore+findAsync"></a>
### neDB.findAsync(query, [projection]) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code> ### neDB.findAsync(query, [projection]) ⇒ <code>Cursor.&lt;Array.&lt;document&gt;&gt;</code>
<p>Async version of [find](#Datastore+find).</p> <p>Find all documents matching the query.
We return the [Cursor](#Cursor) that the user can either <code>await</code> directly or use to can [limit](#Cursor+limit) or
[skip](#Cursor+skip) before.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**Params** **Params**
@ -547,22 +567,23 @@ interpreted as the callback.</p>
<a name="Datastore+findOne"></a> <a name="Datastore+findOne"></a>
### neDB.findOne(query, [projection], [callback]) ⇒ [<code>Cursor.&lt;document&gt;</code>](#document) \| <code>undefined</code> ### neDB.findOne(query, [projection], [callback]) ⇒ [<code>Cursor.&lt;document&gt;</code>](#document) \| <code>undefined</code>
<p>Find one document matching the query.</p> <p>Callback version of [findOneAsync](#Datastore+findOneAsync).</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#findOneAsync
**Params** **Params**
- query [<code>query</code>](#query) - <p>MongoDB-style query</p> - query [<code>query</code>](#query)
- [projection] [<code>projection</code>](#projection) | [<code>SingleDocumentCallback</code>](#SingleDocumentCallback) <code> = {}</code> - <p>MongoDB-style projection</p> - [projection] [<code>projection</code>](#projection) | [<code>SingleDocumentCallback</code>](#SingleDocumentCallback) <code> = {}</code>
- [callback] [<code>SingleDocumentCallback</code>](#SingleDocumentCallback) - <p>Optional callback, signature: err, doc</p> - [callback] [<code>SingleDocumentCallback</code>](#SingleDocumentCallback)
<a name="Datastore+findOneAsync"></a> <a name="Datastore+findOneAsync"></a>
### neDB.findOneAsync(query, projection) ⇒ [<code>Cursor.&lt;document&gt;</code>](#document) ### neDB.findOneAsync(query, projection) ⇒ [<code>Cursor.&lt;document&gt;</code>](#document)
<p>Async version of [findOne](#Datastore+findOne).</p> <p>Find one document matching the query.
We return the [Cursor](#Cursor) that the user can either <code>await</code> directly or use to can [skip](#Cursor+skip) before.</p>
**Kind**: instance method of [<code>Datastore</code>](#Datastore) **Kind**: instance method of [<code>Datastore</code>](#Datastore)
**See**: Datastore#findOne
**Params** **Params**
- query [<code>query</code>](#query) - <p>MongoDB-style query</p> - query [<code>query</code>](#query) - <p>MongoDB-style query</p>

@ -247,7 +247,4 @@ class Cursor {
} }
// Interface // Interface
/**
* @type {Cursor}
*/
module.exports = Cursor module.exports = Cursor

@ -313,19 +313,19 @@ class Datastore extends EventEmitter {
} }
/** /**
* Load the database from the datafile, and trigger the execution of buffered commands if any. * Callback version of {@link Datastore#loadDatabaseAsync}.
* @param {NoParamCallback} callback * @param {NoParamCallback} [callback]
* @see Datastore#loadDatabaseAsync
*/ */
loadDatabase (callback) { loadDatabase (callback) {
if (typeof callback !== 'function') callback = () => {} const promise = this.loadDatabaseAsync()
callbackify(() => this.loadDatabaseAsync())(callback) if (typeof callback === 'function') callbackify(() => promise)(callback)
} }
/** /**
* Async version of {@link Datastore#loadDatabase}. * Load the database from the datafile, and trigger the execution of buffered commands if any.
* @async * @async
* @return {Promise} * @return {Promise}
* @see Datastore#loadDatabase
*/ */
loadDatabaseAsync () { loadDatabaseAsync () {
return this.executor.pushAsync(() => this.persistence.loadDatabaseAsync(), true) return this.executor.pushAsync(() => this.persistence.loadDatabaseAsync(), true)
@ -351,31 +351,35 @@ class Datastore extends EventEmitter {
} }
/** /**
* Ensure an index is kept for this field. Same parameters as lib/indexes * Callback version of {@link Datastore#ensureIndex}.
* This function acts synchronously on the indexes, however the persistence of the indexes is deferred with the
* executor.
* Previous versions said explicitly the callback was optional, it is now recommended setting one.
* @param {object} options * @param {object} options
* @param {string} options.fieldName Name of the field to index. Use the dot notation to index a field in a nested document. * @param {string} options.fieldName
* @param {boolean} [options.unique = false] Enforce field uniqueness. Note that a unique index will raise an error if you try to index two documents for which the field is not defined. * @param {boolean} [options.unique = false]
* @param {boolean} [options.sparse = false] don't index documents for which the field is not defined. Use this option along with "unique" if you want to accept multiple documents for which it is not defined. * @param {boolean} [options.sparse = false]
* @param {number} [options.expireAfterSeconds] - if set, the created index is a TTL (time to live) index, that will automatically remove documents when the system date becomes larger than the date on the indexed field plus `expireAfterSeconds`. Documents where the indexed field is not specified or not a `Date` object are ignored * @param {number} [options.expireAfterSeconds]
* @param {NoParamCallback} callback Callback, signature: err * @param {NoParamCallback} [callback]
* @see Datastore#ensureIndex
*/ */
ensureIndex (options = {}, callback = () => {}) { ensureIndex (options = {}, callback) {
const promise = this.ensureIndexAsync(options) // to make sure the synchronous part of ensureIndexAsync is executed synchronously const promise = this.ensureIndexAsync(options) // to make sure the synchronous part of ensureIndexAsync is executed synchronously
callbackify(() => promise)(callback) if (typeof callback === 'function') callbackify(() => promise)(callback)
} }
/** /**
* Async version of {@link Datastore#ensureIndex}. * Ensure an index is kept for this field. Same parameters as lib/indexes
* This function acts synchronously on the indexes, however the persistence of the indexes is deferred with the
* executor.
* @param {object} options * @param {object} options
* @param {string} options.fieldName Name of the field to index. Use the dot notation to index a field in a nested document. * @param {string} options.fieldName Name of the field to index. Use the dot notation to index a field in a nested
* @param {boolean} [options.unique = false] Enforce field uniqueness. Note that a unique index will raise an error if you try to index two documents for which the field is not defined. * document.
* @param {boolean} [options.sparse = false] Don't index documents for which the field is not defined. Use this option along with "unique" if you want to accept multiple documents for which it is not defined. * @param {boolean} [options.unique = false] Enforce field uniqueness. Note that a unique index will raise an error
* @param {number} [options.expireAfterSeconds] - If set, the created index is a TTL (time to live) index, that will automatically remove documents when the system date becomes larger than the date on the indexed field plus `expireAfterSeconds`. Documents where the indexed field is not specified or not a `Date` object are ignored * if you try to index two documents for which the field is not defined.
* @param {boolean} [options.sparse = false] Don't index documents for which the field is not defined. Use this option
* along with "unique" if you want to accept multiple documents for which it is not defined.
* @param {number} [options.expireAfterSeconds] - If set, the created index is a TTL (time to live) index, that will
* automatically remove documents when the system date becomes larger than the date on the indexed field plus
* `expireAfterSeconds`. Documents where the indexed field is not specified or not a `Date` object are ignored.
* @return {Promise<void>} * @return {Promise<void>}
* @see Datastore#ensureIndex
*/ */
async ensureIndexAsync (options = {}) { async ensureIndexAsync (options = {}) {
if (!options.fieldName) { if (!options.fieldName) {
@ -676,21 +680,20 @@ class Datastore extends EventEmitter {
} }
/** /**
* Insert a new document. * Callback version of {@link Datastore#insertAsync}.
* @param {document|document[]} newDoc * @param {document|document[]} newDoc
* @param {SingleDocumentCallback} [callback = () => {}] Optional callback, signature: err, insertedDoc * @param {SingleDocumentCallback|MultipleDocumentsCallback} [callback]
* * @see Datastore#insertAsync
* @private
*/ */
insert (newDoc, callback) { insert (newDoc, callback) {
if (typeof callback !== 'function') callback = () => {} const promise = this.insertAsync(newDoc)
callbackify(doc => this.insertAsync(doc))(newDoc, callback) if (typeof callback === 'function') callbackify(() => promise)(callback)
} }
/** /**
* Async version of {@link Datastore#insert}. * Insert a new document, or new documents.
* @param {document|document[]} newDoc * @param {document|document[]} newDoc Document or array of documents to insert.
* @return {Promise<document>} * @return {Promise<document|document[]>} The document(s) inserted.
* @async * @async
*/ */
insertAsync (newDoc) { insertAsync (newDoc) {
@ -704,10 +707,11 @@ class Datastore extends EventEmitter {
*/ */
/** /**
* Count all documents matching the query. * Callback-version of {@link Datastore#countAsync}.
* @param {query} query MongoDB-style query * @param {query} query
* @param {Datastore~countCallback} [callback] If given, the function will return undefined, otherwise it will return the Cursor. * @param {Datastore~countCallback} [callback]
* @return {Cursor<number>|undefined} * @return {Cursor<number>|undefined}
* @see Datastore#countAsync
*/ */
count (query, callback) { count (query, callback) {
const cursor = this.countAsync(query) const cursor = this.countAsync(query)
@ -717,7 +721,7 @@ class Datastore extends EventEmitter {
} }
/** /**
* Async version of {@link Datastore#count}. * Count all documents matching the query.
* @param {query} query MongoDB-style query * @param {query} query MongoDB-style query
* @return {Cursor<number>} count * @return {Cursor<number>} count
* @async * @async
@ -727,13 +731,12 @@ class Datastore extends EventEmitter {
} }
/** /**
* Find all documents matching the query * Callback version of {@link Datastore#findAsync}.
* If no callback is passed, we return the cursor so that user can limit, skip and finally exec * @param {query} query
* @param {query} query MongoDB-style query * @param {projection|MultipleDocumentsCallback} [projection = {}]
* @param {projection|MultipleDocumentsCallback} [projection = {}] MongoDB-style projection. If not given, will be * @param {MultipleDocumentsCallback} [callback]
* interpreted as the callback.
* @param {MultipleDocumentsCallback} [callback] Optional callback, signature: err, docs
* @return {Cursor<document[]>|undefined} * @return {Cursor<document[]>|undefined}
* @see Datastore#findAsync
*/ */
find (query, projection, callback) { find (query, projection, callback) {
if (arguments.length === 1) { if (arguments.length === 1) {
@ -753,7 +756,9 @@ class Datastore extends EventEmitter {
} }
/** /**
* Async version of {@link Datastore#find}. * Find all documents matching the query.
* We return the {@link Cursor} that the user can either `await` directly or use to can {@link Cursor#limit} or
* {@link Cursor#skip} before.
* @param {query} query MongoDB-style query * @param {query} query MongoDB-style query
* @param {projection} [projection = {}] MongoDB-style projection * @param {projection} [projection = {}] MongoDB-style projection
* @return {Cursor<document[]>} * @return {Cursor<document[]>}
@ -773,11 +778,12 @@ class Datastore extends EventEmitter {
*/ */
/** /**
* Find one document matching the query. * Callback version of {@link Datastore#findOneAsync}.
* @param {query} query MongoDB-style query * @param {query} query
* @param {projection|SingleDocumentCallback} [projection = {}] MongoDB-style projection * @param {projection|SingleDocumentCallback} [projection = {}]
* @param {SingleDocumentCallback} [callback] Optional callback, signature: err, doc * @param {SingleDocumentCallback} [callback]
* @return {Cursor<document>|undefined} * @return {Cursor<document>|undefined}
* @see Datastore#findOneAsync
*/ */
findOne (query, projection, callback) { findOne (query, projection, callback) {
if (arguments.length === 1) { if (arguments.length === 1) {
@ -797,11 +803,11 @@ class Datastore extends EventEmitter {
} }
/** /**
* Async version of {@link Datastore#findOne}. * Find one document matching the query.
* We return the {@link Cursor} that the user can either `await` directly or use to can {@link Cursor#skip} before.
* @param {query} query MongoDB-style query * @param {query} query MongoDB-style query
* @param {projection} projection MongoDB-style projection * @param {projection} projection MongoDB-style projection
* @return {Cursor<document>} * @return {Cursor<document>}
* @see Datastore#findOne
*/ */
findOneAsync (query, projection = {}) { findOneAsync (query, projection = {}) {
const cursor = new Cursor(this, query, docs => docs.length === 1 ? model.deepCopy(docs[0]) : null) const cursor = new Cursor(this, query, docs => docs.length === 1 ? model.deepCopy(docs[0]) : null)

@ -205,13 +205,11 @@ const writeFileLinesAsync = (filename, lines) => new Promise((resolve, reject) =
}) })
readable.on('error', err => { readable.on('error', err => {
if (err) reject(err) reject(err)
else resolve()
}) })
stream.on('error', err => { stream.on('error', err => {
if (err) reject(err) reject(err)
else resolve()
}) })
} catch (err) { } catch (err) {
reject(err) reject(err)

@ -824,8 +824,16 @@ describe('Persistence async', function () {
const datafileLength = (await fs.readFile('workspace/lac.db', 'utf8')).length const datafileLength = (await fs.readFile('workspace/lac.db', 'utf8')).length
assert(datafileLength > 5000)
// Loading it in a separate process that we will crash before finishing the loadDatabase // Loading it in a separate process that we will crash before finishing the loadDatabase
fork('test_lac/loadAndCrash.test').on('exit', async function (code) { const child = fork('test_lac/loadAndCrash.test', [], { stdio: 'inherit' })
await Promise.race([
new Promise((resolve, reject) => child.on('error', reject)),
new Promise((resolve, reject) => {
child.on('exit', async function (code) {
try {
assert.equal(code, 1) // See test_lac/loadAndCrash.test.js assert.equal(code, 1) // See test_lac/loadAndCrash.test.js
assert.equal(await exists('workspace/lac.db'), true) assert.equal(await exists('workspace/lac.db'), true)
@ -847,16 +855,32 @@ describe('Persistence async', function () {
assert.notEqual(docI, undefined) assert.notEqual(docI, undefined)
assert.deepEqual({ hello: 'world', _id: 'anid_' + i }, docI) assert.deepEqual({ hello: 'world', _id: 'anid_' + i }, docI)
} }
resolve()
} catch (error) {
reject(error)
}
})
})
])
}) })
// Not run on Windows as there is no clean way to set maximum file descriptors. Not an issue as the code itself is tested. // Not run on Windows as there is no clean way to set maximum file descriptors. Not an issue as the code itself is tested.
it('Cannot cause EMFILE errors by opening too many file descriptors', async function () { it('Cannot cause EMFILE errors by opening too many file descriptors', async function () {
this.timeout(5000) this.timeout(10000)
if (process.platform === 'win32' || process.platform === 'win64') { return } if (process.platform === 'win32' || process.platform === 'win64') { return }
const { stdout } = await promisify(execFile)('test_lac/openFdsLaunch.sh') try {
const { stdout, stderr } = await promisify(execFile)('test_lac/openFdsLaunch.sh')
// The subprocess will not output anything to stdout unless part of the test fails // The subprocess will not output anything to stdout unless part of the test fails
if (stdout.length !== 0) throw new Error(stdout) if (stderr.length !== 0) {
}) console.error('subprocess catch\n', stdout)
throw new Error(stderr)
}
} catch (err) {
if (Object.prototype.hasOwnProperty.call(err, 'stdout') || Object.prototype.hasOwnProperty.call(err, 'stderr')) {
console.error('subprocess catch\n', err.stdout)
throw new Error(err.stderr)
} else throw err
}
}) })
}) // ==== End of 'Prevent dataloss when persisting data' ==== }) // ==== End of 'Prevent dataloss when persisting data' ====

@ -1004,6 +1004,8 @@ describe('Persistence', function () {
const datafileLength = fs.readFileSync('workspace/lac.db', 'utf8').length const datafileLength = fs.readFileSync('workspace/lac.db', 'utf8').length
assert(datafileLength > 5000)
// Loading it in a separate process that we will crash before finishing the loadDatabase // Loading it in a separate process that we will crash before finishing the loadDatabase
fork('test_lac/loadAndCrash.test').on('exit', function (code) { fork('test_lac/loadAndCrash.test').on('exit', function (code) {
code.should.equal(1) // See test_lac/loadAndCrash.test.js code.should.equal(1) // See test_lac/loadAndCrash.test.js

@ -1,133 +1,59 @@
/* eslint-env mocha */ /* eslint-env mocha */
/* global DEBUG */
/** /**
* Load and modify part of fs to ensure writeFile will crash after writing 5000 bytes * Load and modify part of fs to ensure writeFile will crash after writing 5000 bytes
*/ */
const fs = require('fs') const fs = require('fs')
const { Writable } = require('stream')
const { callbackify } = require('util')
function rethrow () { fs.promises.writeFile = async function (path, data) {
// Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and let onePassDone = false
// is fairly slow to generate. const options = { encoding: 'utf8', mode: 0o666, flag: 'w' } // we don't care about the actual options passed
if (DEBUG) {
const backtrace = new Error()
return function (err) {
if (err) {
backtrace.stack = err.name + ': ' + err.message +
backtrace.stack.substr(backtrace.name.length)
throw backtrace
}
}
}
return function (err) {
if (err) {
throw err // Forgot a callback but don't know where? Use NODE_DEBUG=fs
}
}
}
function maybeCallback (cb) {
return typeof cb === 'function' ? cb : rethrow()
}
function isFd (path) {
return (path >>> 0) === path
}
function assertEncoding (encoding) {
if (encoding && !Buffer.isEncoding(encoding)) {
throw new Error('Unknown encoding: ' + encoding)
}
}
let onePassDone = false
function writeAll (fd, isUserFd, buffer, offset, length, position, callback_) { const filehandle = await fs.promises.open(path, options.flag, options.mode)
const callback = maybeCallback(arguments[arguments.length - 1]) const buffer = (data instanceof Buffer) ? data : Buffer.from('' + data, options.encoding || 'utf8')
let length = buffer.length
let offset = 0
try {
while (length > 0) {
if (onePassDone) { process.exit(1) } // Crash on purpose before rewrite done if (onePassDone) { process.exit(1) } // Crash on purpose before rewrite done
const l = Math.min(5000, length) // Force write by chunks of 5000 bytes to ensure data will be incomplete on crash const { bytesWritten } = await filehandle.write(buffer, offset, Math.min(5000, length)) // Force write by chunks of 5000 bytes to ensure data will be incomplete on crash
// write(fd, buffer, offset, length, position, callback)
fs.write(fd, buffer, offset, l, position, function (writeErr, written) {
if (writeErr) {
if (isUserFd) {
if (callback) callback(writeErr)
} else {
fs.close(fd, function () {
if (callback) callback(writeErr)
})
}
} else {
onePassDone = true onePassDone = true
if (written === length) { offset += bytesWritten
if (isUserFd) { length -= bytesWritten
if (callback) callback(null)
} else {
fs.close(fd, callback)
}
} else {
offset += written
length -= written
if (position !== null) {
position += written
} }
writeAll(fd, isUserFd, buffer, offset, length, position, callback) } finally {
await filehandle.close()
} }
}
})
} }
fs.writeFile = function (path, data, options, callback_) { class FakeFsWriteStream extends Writable {
const callback = maybeCallback(arguments[arguments.length - 1]) constructor (filename) {
super()
if (!options || typeof options === 'function') { this.filename = filename
options = { encoding: 'utf8', mode: 438, flag: 'w' } // Mode 438 == 0o666 (compatibility with older Node releases) this._content = Buffer.alloc(0)
} else if (typeof options === 'string') {
options = { encoding: options, mode: 438, flag: 'w' } // Mode 438 == 0o666 (compatibility with older Node releases)
} else if (typeof options !== 'object') {
throw new Error(`throwOptionsError${options}`)
} }
assertEncoding(options.encoding) _write (chunk, encoding, callback) {
this._content = Buffer.concat([this._content, Buffer.from(chunk, encoding)])
const flag = options.flag || 'w' callback()
if (isFd(path)) {
writeFd(path, true)
return
} }
fs.open(path, flag, options.mode, function (openErr, fd) { _end (chunk, encoding, callback) {
if (openErr) { this._content = Buffer.concat([this._content, Buffer.from(chunk, encoding)])
if (callback) callback(openErr) callback()
} else {
writeFd(fd, false)
} }
})
function writeFd (fd, isUserFd) {
const buffer = (data instanceof Buffer) ? data : Buffer.from('' + data, options.encoding || 'utf8')
const position = /a/.test(flag) ? null : 0
writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback)
}
}
fs.createWriteStream = function (path) {
let content = ''
return {
write (data) {
content += data
},
close (callback) { close (callback) {
fs.writeFile(path, content, callback) callbackify(fs.promises.writeFile)(this.filename, this._content, 'utf8', callback)
}
} }
} }
// End of fs modification fs.createWriteStream = path => new FakeFsWriteStream(path)
// End of fs monkey patching
const Nedb = require('../lib/datastore.js') const Nedb = require('../lib/datastore.js')
const db = new Nedb({ filename: 'workspace/lac.db' }) const db = new Nedb({ filename: 'workspace/lac.db' })
db.loadDatabase() db.loadDatabaseAsync() // no need to await

@ -1,65 +1,61 @@
const fs = require('fs') const fs = require('fs')
const { waterfall, whilst } = require('../test/utils.test.js') const fsPromises = fs.promises
const Nedb = require('../lib/datastore') const Nedb = require('../lib/datastore')
const { callbackify } = require('util')
const db = new Nedb({ filename: './workspace/openfds.db', autoload: true })
const N = 64 const N = 64
let i
let fds
function multipleOpen (filename, N, callback) { // A console.error triggers an error of the parent test
whilst(function () { return i < N }
, function (cb) {
fs.open(filename, 'r', function (err, fd) {
i += 1
if (fd) { fds.push(fd) }
return cb(err)
})
}
, callback)
}
waterfall([ const test = async () => {
// Check that ulimit has been set to the correct value let filehandles = []
function (cb) { try {
i = 0 for (let i = 0; i < 2 * N + 1; i++) {
fds = [] const filehandle = await fsPromises.open('./test_lac/openFdsTestFile', 'r')
multipleOpen('./test_lac/openFdsTestFile', 2 * N + 1, function (err) { filehandles.push(filehandle)
if (!err) { console.log('No error occured while opening a file too many times') } }
fds.forEach(function (fd) { fs.closeSync(fd) }) console.error('No error occurred while opening a file too many times')
return cb() process.exit(1)
}) } catch (error) {
}, if (error.code !== 'EMFILE') {
function (cb) { console.error(error)
i = 0 process.exit(1)
fds = [] }
multipleOpen('./test_lac/openFdsTestFile2', N, function (err) { } finally {
if (err) { console.log('An unexpected error occured when opening file not too many times: ' + err) } for (const filehandle of filehandles) {
fds.forEach(function (fd) { fs.closeSync(fd) }) await filehandle.close()
return cb() }
}) filehandles = []
}, }
// Then actually test NeDB persistence
function () {
db.remove({}, { multi: true }, function (err) {
if (err) { console.log(err) }
db.insert({ hello: 'world' }, function (err) {
if (err) { console.log(err) }
i = 0 try {
whilst(function () { return i < 2 * N + 1 } for (let i = 0; i < N; i++) {
, function (cb) { const filehandle = await fsPromises.open('./test_lac/openFdsTestFile2', 'r')
callbackify(() => db.persistence.persistCachedDatabaseAsync())(function (err) { filehandles.push(filehandle)
if (err) { return cb(err) }
i += 1
return cb()
})
} }
, function (err) { } catch (error) {
if (err) { console.log('Got unexpected error during one peresistence operation: ' + err) } console.error(`An unexpected error occurred when opening file not too many times at i: ${i} with error: ${error}`)
process.exit(1)
} finally {
for (const filehandle of filehandles) {
await filehandle.close()
} }
)
})
})
} }
], () => {})
try {
const db = new Nedb({ filename: './workspace/openfds.db' })
await db.loadDatabaseAsync()
await db.removeAsync({}, { multi: true })
await db.insertAsync({ hello: 'world' })
for (let i = 0; i < 2 * N + 1; i++) {
await db.persistence.persistCachedDatabaseAsync()
}
} catch (error) {
console.error(`Got unexpected error during one persistence operation at ${i}: with error: ${error}`)
}
}
try {
test()
} catch (error) {
console.error(error)
process.exit(1)
}

Loading…
Cancel
Save