Search code examples
androidmultithreadingormtransactionsgreendao

Can you encapsulate multiple nested transactions across different threads into an overall transaction with greenDAO?


I am working on an Android application that uses greenDAO as a data persistence layer. The application downloads data from various different sources across multiple threads (determined by a thread pool), each piece of data is inserted into the database in a transaction using insertOrReplaceInTx. This is working fine.

My question is whether it is technically possible, using greenDAO, to encapsulate these different transactions (which occur on different threads) into an overall transaction, using nested transactions. I know in theory it is possible to do this if all the transactions were taking place on a single thread, however I am unsure if this possible with the insertOrReplaceInTx calls occurring on different threads.

The reason I wish to encapsulate these into a single overall transaction is because they represent a synchronisation process within an app. In the event of any single part of the import failing, I wish to abort and rollback all of the modifications within the overall transaction.

If I begin a transaction with db.beginTransaction on the main thread where I initiate the import process, this creates a deadlock when another thread tries to insertOrReplaceInTxt.

Is the correct way to counter this to ensure that all greenDAO transactions are taking place on the same thread?


Solution

  • Afaik, you cannot because each thread manages its own connection.

    If you have such dependency between these operations, you probably want to sync them anyways.

    e.g. what if Job A finishes way before Job B and Job B's db connection fails. Your data will go out of sync again. You still need some logic for the other job. Also, writers are mutually exclusive.

    I would suggest creating a utility class that can run a list of runnables in a transaction. Each job, when finished, enqueues a Runnable to this utility. These runnables include the actual database commands.

    When the last one arrives (this depends on your dependency logic), the utility will run all runnables in a transaction.

    A sample implementation may look like this: (I used a simple counter but you may need a more complex logic)

    class DbBundle {
        AtomicInteger mLatch;
        List<Runnable> mRunnables = new ArrayList();
        DbBundle(int numberOfTx) {
            mLatch = new AtomicInteger(numberOfTx);
        }
    
        void cancel() {
            mLatch.set(-1); // so decrement can never reach 0 in submit
        }
    
        boolean isCanceled() {
            mLatch.count() < 0;
        }
    
        void submit(Runnable runnable) {
            mRunnables.add(runnable);
            if (mLatch.decrementAndGet() == 0) {
                 db.beginTransaction();
                 try {
                     for (Runnable r : mRunnables) r.run();
                     db.setTransactionSuccessful()
                 } finally {
                      db.endTransaction();
                 }
            } 
        }
    }
    

    When you create each job, you pass this shared DbBundle and the last one will execute them all. So a job would look like:

    ....
    try {
        if (!dbBundle.isCanceled()) { // avoid extra request if it is already canceled
            final List<User> users = webservice.getUsers();
            dbBundle.submit(new Runnable() {
                void onRun() {
                     saveUsers(users);//which calls db. no transaction code.
                });
        });
    } catch(Throwable t) {
        dbBundle.cancel();
    }