Skip to content

When processing an update of same value for multiple fields in a document, update it in single operation #54

@mitar

Description

@mitar

For example, if a document TestDocument has multiple referencing User collection, and a particular TestDocument document is referencing one document in User, when the latter is removed, we should make only single operation to update all those fields in the TestDocument document.

Currently we do multiple operations, which means that further observe callbacks are triggered but now the document referenced in fields not yet updated does not exist, so an error is shown.

It seems not not all operations can be yet be made so (see https://jira.mongodb.org/browse/SERVER-6566), but ones wher we loop and update each element in an array could since 3.5.2 (see https://jira.mongodb.org/browse/SERVER-1243).

Meteor 1.7 updated MongoDB to 3.6.4 so for those versions we could update our looping updates over array elements to do that in one operation.

If we could do also other operations all together then we should probably at every change do one update to update all fields at once. Current logic setups multiple observes for each field and is updating one by one.

A test for the example above is:

import {Meteor} from 'meteor/meteor';
import {Document} from 'meteor/peerlibrary:peerdb';

import Future from 'fibers/future';

class User extends Document {}

User.Meta({
  name: 'User',
});

class TestDocument extends Document {}

TestDocument.Meta({
  name: 'TestDocument',
  fields(fields) {
    return _.extend(fields, {
      userPermissions: [
        {
          user: Document.ReferenceField(User, {_id: 1}),
          addedBy: Document.ReferenceField(User, {_id: 1}, false),
        },
      ],
    });
  },
});

User.documents.remove({_id: {$in: ['aaaa', 'bbbb']}});
TestDocument.documents.remove({_id: 'cccc'});

const WAIT_FOR_DATABASE_TIMEOUT = 1500; // ms

function waitForDatabase() {
  const future = new Future();

  let timeout = null;
  const newTimeout = function () {
    if (timeout) {
      Meteor.clearTimeout(timeout);
    }
    timeout = Meteor.setTimeout(function () {
      timeout = null;
      if (!future.isResolved()) {
        future.return();
      }
    }, WAIT_FOR_DATABASE_TIMEOUT);
  };

  newTimeout();

  const handles = [];
  for (const document of Document.list) {
    handles.push(document.documents.find({}).observeChanges({
      added(id, fields) {
        newTimeout();
      },
      changed(id, fields) {
        newTimeout();
      },
      removed(id) {
        newTimeout();
      },
    }));
  }

  future.wait();

  for (const handle of handles) {
    handle.stop();
  }
}

Meteor.startup(function () {
  User.documents.insert({
    _id: 'aaaa',
  });
  User.documents.insert({
    _id: 'bbbb',
  });

  TestDocument.documents.insert({
    _id: 'cccc',
    userPermissions: [
      {
        user: {
          _id: 'aaaa',
        },
        addedBy: {
          _id: 'aaaa',
        },
      },
      {
        user: {
          _id: 'bbbb',
        },
        addedBy: {
          _id: 'aaaa',
        },
      },
    ],
  });

  waitForDatabase();

  User.documents.remove({_id: 'aaaa'});
});

When this is ran the following error is seen:

Document 'TestDocument' 'cccc' field 'userPermissions.addedBy' is referencing a nonexistent document 'aaaa'

This is because subdocument with user_.id == 'aaaa' is pulled from the array, which triggers an update with subdocument with addedBy._id === 'aaaa' still there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions