Working with transactions

Most database APIs have a way of dealing with transactions – a set of operations all of which much work, and a way of rolling back if they don’t
Indeed many of the back-ends implemented here have this capability, but I haven’t yet exposed that through the abstraction layer. The main reason for this is that many of the back-ends are not actually databases, and therefore don’t have a roll back capability or the concept of transactions. 
However since I had always planned to introduce rollback, I’ve implemented these kind of databases to use  DriverMemory first, before committing to the back-end. This means that I can now start to implement transactions whether simulated, or using the transactional capabilities of the back-end.

Here’s how it works. 

  • Transactions are accessible through the normal DbAbstraction  handler object, and you use an anonymous function to execute your function. 
  • A normal result package should be returned that can be tested for result.handleCode < 0 meaning failure. 
  • A transaction failure will be signaled with result.transaction.code < 0 
  • If a failure is detected, a rollback to before the transaction will be executed 
  • There will be a lock taken out for the  entire transaction
  • Although caching can be active in each of the individual  items of the transaction as usual, there is no caching of the transaction result itself.
  • The options will be used to modify the transaction behavior, or to pass additional info to your function – to be dealt with later
  • Today various operations are locked to preserve consistency in multi user environments. With transactions a lock is taken out around the transaction, not for the individual operations within the transaction. For operations not enclosed in transactions, by default locking is enabled for operations that write, but not for read. However there is an option to lock both read and write operations. 

Some notes on performance

  • For back-ends that emulate databases (so far sheets, drive and properties), you’ll see a performance improvement for everything if you wrap multiple operations in transactions. This is because the whole thing is done in memory. The down side is that nothing else (that needs a lock) can operate on that database during the transaction. 
  • Aggressive locking can hurt performance when there are many simultaneous users, but you can use to guarantee consistency between writes/reads
  • Operations within transactions are must faster in aggregate than separate ones. However you do lock out others for the duration of the transaction. Another side effect is that if you follow a write with a read, some databases haven’t yet properly registered by the time the read comes, and will return the wrong count. This is especially true with Orchestrate. 

You can set locking when you open the database – for example. 



		


values for locking are 



		


values for transactions are



		


If you have some code wrapped in transactions, and you disable transactions (or the driver does not yet support them), then each operations will be performed as today, but following the setting for locking.

You wrap operations in a transaction like this



		


Example

Sheets are a particular problem, since they don’t have stable IDs (row numbers are used as IDs), and since the design principle here is not to do anything that affects the sheet’s standalone nature (anybody can change it at any time), the row number of an item can change at any time. It’s especially important to wrap operations that rely on previous operations inside a transaction. Below is a query, where the keys are used by a subsequent get. Wrapping the entire thing in a transaction ensure that some other user does not affect the row numbers in between



		


another example – this time we are doing an update. It’s important that the row number IDs don’t change between the query that finds them, and the subsequent update, so wrapping them in a transaction ensures this.



		


What handler.transaction() does 



		


What an enabled driver will do

  • self.transactionCapable = true;
  • self.transactionAware = true;
  • detect if it is in a transaction, and not do any locking itself. 
  • Here is a sheet save(), instrumented for transactions.


		


However sheets, properties and drive can delegate all their workload to the memory driver in a transaction, since there will be no other updates going on. This makes things much more efficient and also means that they can share the delegated code which looks like this.

What a failed transaction looks like:

Here’s a deliberate error in a transaction – I’ve spelt remove wrongly, so I want it to rollback the save operation.



		


and the result



		


A note on keys

As previously noted, some drivers don’t have the capability to store unique keys. In this case a unique key is generated for them temporarily. This is how the operations within a transaction can communicate with each other. This means that you can’t always rely on keys being the same inside and outside transactions. If you are dealing with keys (for example get/update), it’s always best to wrap those in a transaction. For example, with a sheet driver…



		


result.handleKeys, will contain row numbers at that point in time – which may change at any time by other user operations, so they should not be relied on – so the following may not return the right data (it will detect it has changed and give you an error)



		


Whereas



		


will return the correct value since the operations wont be interrupted mid flight by another update instance. Therefore, if you are using keys for database back-ends without a unique key capability, you should keep them isolated to within transactions.

DbAbstraction full code



		


For more on this see Parallel processing in Apps Script and  Database abstraction with google apps script

[do_widget id=jetpack_widget_social_icons-7] [do_widget id=text-5]