5

I've created this object which contains an array, which serves as a work queue.

It kind of works like this:

var work1 = new Work();
var work2 = new Work();
var queue = Workqueue.instance();

queue.add(work1) // Bluebird promise.
.then(function addWork2() {
  return queue.add(work2);
})
.then(function toCommit() {
  return queue.commit();
})
.then(function done(results) {
  // obtain results here.
})
.catch(function(err){});

It works in that case and I can commit more than one task before I call the commit.

However if it's like this:

var work1 = new Work();
var work2 = new Work();
var queue = Workqueue.instance();

queue.add(work1)
.then(function toCommit1() {
  return queue.commit();
})
.then(function done1(result1) {
  // obtain result1 here.
})
.catch(function(err){});

queue.add(work2)
.then(function toCommit2() {
  return queue.commit();
})
.then(function done2(result2) {
  // obtain result2 here.
})
.catch(function(err){});

Something may go wrong, because if the first commit is called after the second commit (two works/tasks are already added), the first commit handler expects a result but they all go to the second commit handler.

The task involves Web SQL database read and may also involves network access. So it's basically a complicated procedure so the above described problem may surface. If only I can have a addWorkAndCommit() implemented which wraps the add and commit together, but still there is no guarantee because addWorkAndCommit() cannot be "atomic" in a sense because they involves asynchronous calls. So even two calls to addWorkAndCommit() may fail. (I don't know how to describe it other than by "atomic", since JavaScript is single-threaded, but this issue crops up).

What can I do?

huggie
  • 17,587
  • 27
  • 82
  • 139

2 Answers2

3

The problem is that there is a commit() but no notion of a transaction, so you cannot explicitly have two isolated transactions running in parallel. From my understanding the Javascript Workqueue is a proxy for a remote queue and the calls to add() and commit() map directly to some kind of remote procedure calls having a similar interface without transactions. I also understand that you would not care if the second add() actually happened after the first commit(), you just want to write two simple subsequent addWorkAndCommit() statements without synchronizing the underlying calls in client code.

What you can do is write a wrapper around the local Workqueue (or alter it directly if it is your code), so that each update of the queue creates a new transaction and a commit() always refers to one such transaction. The wrapper then delays new updates until all previous transactions are committed (or rolled back).

lex82
  • 11,173
  • 2
  • 44
  • 69
  • Oh wow! What a simple solution. An eureka moment for what a transaction really means.Thanks I was banging my head over this trying to implement synchronicity. – huggie Jan 17 '16 at 11:36
  • The implementation that's in my head right now is that the transaction should be an object that should be exposed to the client of the queue. With the client explicitly knowing which transaction the work is added into. Is that correct? – huggie Jan 17 '16 at 11:41
  • It depends. If you want separate add() and commit() methods in your interface, then yes. If you have addWorkAndCommit() as one method then the caller doesn't need to know anything about transactions. – lex82 Jan 17 '16 at 11:47
1

Adopting Benjamin Gruenbaum's recommendation to use a disposer pattern, here is one, written as an adapter method for Workqueue.instance() :

Workqueue.transaction = function (work) { // `work` is a function
    var queue = this.instance();
    return Promise.resolve(work(queue)) // `Promise.resolve()` avoids an error if `work()` doesn't return a promise.
    .then(function() {
        return queue.commit();
    });
}

Now you can write :

// if the order mattters, 
// then add promises sequentially.
Workqueue.transaction(function(queue) {
    var work1 = new Work();
    var work2 = new Work();
    return queue.add(work1)
    .then(function() {
        return queue.add(work2);
    });
});
// if the order doesn't mattter, 
// add promises in parallel.
Workqueue.transaction(function(queue) {
    var work1 = new Work();
    var work2 = new Work();
    var promise1 = queue.add(work1);
    var promise2 = queue.add(work2);
    return Promise.all(promise1, promise2);
});
// you can even pass `queue` around
Workqueue.transaction(function(queue) {
    var work1 = new Work();
    var promise1 = queue.add(work1);
    var promise2 = myCleverObject.doLotsOfAsyncStuff(queue);
    return Promise.all(promise1, promise2);
});

In practice, an error handler should be included like this - Workqueue.transaction(function() {...}).catch(errorHandler);

Whatever you write, all you need to do is ensure that the callback function returns a promise that is an aggregate of all the component asynchronisms (component promises). When the aggregate promise resolves, the disposer will ensure that the transaction is committed.

As with all disposers, this one doesn't do anything you can't do without it. However it :

  • serves as a reminder of what you are doing by providing a named .transaction() method,
  • enforces the notion of a single transaction by constraining a Workqueue.instance() to one commit.

If for any reason you should ever need to do two or more commits on the same queue (why?), then you can always revert to calling Workqueue.instance() directly.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • What a beautiful solution! Thank you so much. – huggie Jan 19 '16 at 04:46
  • Should the concept of transaction be extended to every layer of interface? It seems like when I have a transaction, error still props up. The work result can still be undefined. – huggie Jan 25 '16 at 05:46
  • Mmmm, not too sure. Does the other solution exhibit the same issue? – Roamer-1888 Jan 25 '16 at 18:12
  • I'm looking back to this code again, when Promise.resolve(work(queue)) is "resolving", hasn't Promise.all(promise1, promise2)` already finished executing? They're not actually executing at some point within the commit, right? – huggie Aug 04 '16 at 05:15
  • Never mind, my mistake, I need to refresh my memory a bit. – huggie Aug 04 '16 at 07:51
  • Huggie, seems like you've worked it out but, for the record, `Promise.all(promise1, promise2)` executes immediately and returns a promise. That returned promise will only *settle* in response to `promise1` and/or `promise2` settling. – Roamer-1888 Aug 04 '16 at 12:40