Implementation uses Meteor.EnvironmentVariable
to store the MongoDB session and
MongoInternals.NpmModule.Collection
for which it overrides each method that supports sessions and passes the session stored in Meteor.EnvironmentVariable
.
This implementation is relying on fibers and expects Meteor server side sync code.
If function passed to runInTransaction
does not throw an error, the package will try to commit the transaction. Note that the commit itself might throw an error.
If an error is thrown inside the function passed to runInTransaction
, transaction will be aborted and error thrown.
// server-side
import {runInTransaction} from 'meteor/bhunjadi:mongo-transactions';
const Invoice = new Mongo.Collection('invoice');
const LineItems = new Mongo.Collection('lineItems');
const Payments = new Mongo.Collection('payments');
function (invoice, lineItems) {
const paymentId = runInTransaction(() => {
// sync code which uses fibers
Invoice.insert(invoice);
LineItems.insert(lineItems);
const payment = createPaymentDoc();
// You can return anything from this function
return Payments.insert(payment);
});
}
import {runInTransaction} from 'meteor/bhunjadi:mongo-transactions';
try {
runInTransaction(() => {
// do something within this transaction
performDBWork();
throw new Error('My error');
});
}
catch (e) {
// "e" is the same error thrown above
// transaction is already aborted
handleError(e);
}
Consider this example:
runInTransaction(() => {
Invoice.insert({}, () => {
InvoiceItem.insert({});
})
throw new Error('fail');
});
Since the function threw the error at the end, we might expect that both Invoice
and InvoiceItem
will not be inserted into the database.
However, this code usually fails in with exception outside the main execution context (because we're using async functions):
- Exception in callback of async function: MongoError: Use of expired sessions is not permitted
- MongoError: Transaction N has been aborted.
Both of these mean the same, transaction has already been aborted and session is expired.
Consequently, throwing an error from the async function will not throw a "sync" error and won't abort transaction:
runInTransaction(() => {
Invoice.insert({}, (err, res) => {
InvoiceItem.insert({});
throw new Error('fail');
});
// with or without it, transaction won't be aborted
Meteor._sleepForMs(1000);
});
Setting any value in Meteor._sleepForMs()
function won't work, either.
There are some questions that might arise here:
-
Why does async function even get a session when we know it is executed after
runInTransaction
has exited?Behind the scenes, Meteor wraps the callback method with
Meteor.wrapAsync
, see here.Meteor.wrapAsync
usesMeteor.bindEnvironment
to wrap the callback and use fibers.See the comment for
bindEnvironment
function:Meteor application code is always supposed to be run inside a fiber. bindEnvironment ensures that the function it wraps is run from inside a fiber and ensures it sees the values of Meteor environment variables that are set at the time bindEnvironment is called.
This means that our
sessionVariable
(which is instance ofMeteor.EnvironmentVariable
) returns the session that was part ofrunInTransaction
function. -
Why are you not checking if session has ended and depending on this send
session
object to mongo function?While this might work, it might yield unexpected results. If we'd do it like that, in the example above
InvoiceItem
document would be inserted andInvoice
wouldn't, which might not be the desired outcome.Right now, I envisage we add a parameter to
runInTransaction
to define behaviour for async function and whether this packages should just ignore expired sessions.
This won't work out of the box with async code or any other package that uses async code, for example matb33:collection-hooks
.
For now it looks like you could try to delay the execution of runInTransaction
with Meteor._sleepForMs(n)
. n
is a number in milliseconds that you expect for all async functions to take.
There is no workaround if you need an Error in async callback to abort the transaction.
runInTransaction<R>(fn: () => R, options?: RunInTransactionOptions): R;
fn - sync method to be run in transaction, promises are not supported (yet)
options - optional SessionOptions, https://mongodb.github.io/node-mongodb-native/3.6/api/global.html#SessionOptions
isInTransaction(): boolean;
Whether we already are in the transaction.
sessionVariable: Meteor.EnvironmentVariable
This is the actual Meteor.EnvironmentVariable
instance used by the package.
You could get MongoDB's ClientSession instance with sessionVariable.get()
.
It will return undefined
if there is no session.
cd test-app
meteor npm i
# using browser driver
TEST_BROWSER_DRIVER=puppeteer MONGO_URL= ROOT_URL= meteor test-packages --port=3000 --driver-package meteortesting:mocha ../
- Support promises in combination with fibers