1

I was wondering what is the correct way to organize my class hierarchy in the following situation.
I wanted to build an abstraction around postgresql advisory lock.

Note just for context: An advisory lock is a lock that you can obtain at a session or transaction level. Postgres handle all the complexity for you.

The code that I've written so far is something like

interface DBLockService

interface SessionLockService : DBLockService {
    fun acquire(id: Long)
    fun unlock(id: Long): Boolean
}


interface TransactionalLockService : DBLockService {
    fun txAcquire(id: Long)
}

abstract class BaseDBLockService(protected val entityManager: EntityManager): DBLockService {

    protected fun executeAcquire(preparedStatement: String, id: Long) {
        executeAcquire<Any>(preparedStatement, id)
    }

    protected inline fun <reified T> executeAcquire(preparedStatement: String, id: Long) =
        entityManager
            .createNativeQuery(preparedStatement, T::class.java)
            .setParameter("id", id)
            .singleResult as T
}

@Component
class LockServiceImpl(
    entityManager: EntityManager
) : BaseDBLockService(entityManager),
    SessionLockService {
    companion object {
        const val acquireStatement = "SELECT pg_advisory_lock(:id)"
        const val unlockStatement = "SELECT pg_advisory_unlock(:id)"
    }

    override fun acquire(id: Long) {
        executeAcquire(acquireStatement, id)
    }

    override fun unlock(id: Long) =
        executeAcquire<Boolean>(unlockStatement, id)

}

@Component
class TransactionalLockServiceImpl(
    entityManager: EntityManager
) : BaseDBLockService(entityManager),
    TransactionalLockService {
// very similar implementation
}

Looking at this code there is something that tell's me that there is something wrong:

  • DBLockService is a bit useless interface, there is no method
  • Are SessionLockService and TransactionalLockService just an implementation detail? Is it correct that there is a different interface for every "type" of lock?

But at the same time, if I remove the DBLockService seems very odd to me that there are 2 interfaces (SessionLockService and TransactionalLockService) with very similar context that are not related in any way.
Moreover, removing DBLockService, I'll have the 2 implementations (LockServiceImpl and TransactionalLockServiceImpl) that extends from the abstract class BaseDBLockService to implement these 2 interfaces but at the same time the abstract class is not related to them.

What to you think?
Thanks


Update

As requested I'll add an example of a real case scenario


@Service
class SomethingService(private val lockService: TransactionalLockService){
    
    @Transactional
    fun aMethod(entityId: Long){
        lockService.txAcquire(entityId)
        //code to be synchronized or there will be problems
    }
}

I would like to inject a class of a generic LockService but I cannot find a way to abstract that because imho a lock that disappear after the transaction ends is a lock different from a lock that disappear after the connection to the db is closed (session lock) that is different from a lock that need to be unlocked automatically.
It's possible that there are a lot of other implementations of lock, for example a TimeoutLock that remove the lock after some time.
But I'm not able to think how to separate these implementation details from the general concept of a Lock.

4
  • Without knowing anything specific about Postgres advisory locks, it would seem that there isn't any operation which you can do to both a Session and Transactional lock, so not having an interface in common seems right. Commented Oct 19, 2021 at 21:51
  • @tgdavies actually both can acquire a lock BUT they do it in a different way. For example a session lock can be unlocked but a transactional lock, will unlock himself at the end of the transaction automatically. So adding acquire in the common interface seems a bit odd to me because it's not clear what is the contract of that method Commented Oct 19, 2021 at 22:25
  • Can you give an brief example of how you imagine client code would use said service? I don't know much about Postgres locks either, but Service seems like a bit of a misnomer to me. Maybe Strategy is more appropriate? I image some type of Connection class would use either and/or both implementations, is that correct? Commented Oct 20, 2021 at 13:35
  • 1
    @DecentDabbler I added an example as requested Commented Oct 20, 2021 at 14:22

1 Answer 1

0

Okay, thanks for the example. I still find it a bit odd to call what you want to implement a Service. I'd probably call it a Strategy, but that's not really that important. It's just a semantic preference.

Anyway, what I would do is probably something like the following (untested/pseudo code):

interface LockService {
  Boolean acquire(Long id);
  Boolean unlock(Long id);
}

abstract class BaseLockService
  implements LockService {

  protected EntityManager entityManager;
  
  BaseLockService(EntityManager entityManager) {
    this.entityManager = entityManager;
  }
  
  protected Boolean executeAcquire(String preparedStatement, Long id) {
    // your generic implementation
  }
}

class SessionLockService
  extends BaseLockService {
  
  private static class Statements {
    static final String acquireStatement = "...";
    static final String unlockStatement = "...";
  }
  
  SessionLockService(EntityManager entityManager) {
    super(entityManager);
  }
  
  @Override
  Boolean acquire(Long id) {
    return executeAcquire(Statements.acquireStatement, id);
  }
  
  @Override
  Boolean unlock(Long id) {
    return executeAcquire(Statements.unlockStatement, id);
  }
}

class TransactionalLockService
  extends BaseLockService {
  
  private static class Statements {
    static final String acquireStatement = "...";
  }
  
  TransactionalLockService(EntityManager entityManager) {
    super(entityManager);
  }
  
  @Override
  Boolean acquire(Long id) {
    return executeAcquire(Statements.acquireStatement, id);
  }
  
  @Override
  Boolean unlock(Long id) {
    // simply return true
    return true;
    
    // or if there's some Postgres or EntityManager mechanism to find out if the transaction is still active:
    return !entityManager.isInTransaction(id);
  }
}

class SomeService {

  private final LockService lockService;

  SomeService(LockService lockService) {
    this.lockService = lockService;
  }
  
  void aMethod(Long entityId) {
    if(!lockService.acquire(entityId)) {
      throw new SomeException();
    }
    
    // do code that needs lock
      
    if(!lockService.unlock(entityId)) {
      throw new SomeException();
    }
  }
}

So basically, I would use a common interface and just make TransactionalLockService.unlock() sort of a no-op function that always returns true or, if achievable, more desirable: return the result of some probe mechanism to find out if the transaction with id has correctly ended.

Another idea would be to have a Lock interface, that a LockService returns (very abbreviated example):

interface Lock {
  Boolean unlock();
}

interface LockService {
  Lock acquire(Long id);
}

class TransactionalLock 
  implements Lock {

  private Long id;

  TransactionalLock(Long id) {
    this.id = id;
  }

  @Override
  Boolean unlock() {
    // again, either simply return true
    return true;

    // ...or some result that verifies the transaction has ended
    return verifyTransactionHasEnded(id);
  }
}

class SomeService {

  private final LockService lockService;

  SomeService(LockService lockService) {
    this.lockService = lockService;
  }
  
  void aMethod(Long entityId) {
    Lock lock = lockService.acquire(entityId);
    if(lock == null) {
      throw new SomeException();
    }
    
    // do code that needs lock
      
    if(!lock.unlock()) {
      throw new SomeException();
    }
  }
}

...etc., but that could get very complex very fast, because the Lock implementations need their own mechanism to unlock themselves.

This might still not be exactly what you're looking for, but hopefully it gives you some ideas.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you very much for the answer! These 2 approach are very interesting but there is a problem imho. When injecting a LockService and call lock, how do you know if the lock is still active (and you code thread safe) or not? In a session lock for example you have thread safe code across multiple transaction until u call unlock BUT if the implementation u received is a TransactionalLock this is not true. So the code you are going to use "assume" to know the kind of lock you received that is basically against the idea of using an interface
@Taz Sorry Taz, your additional questions make the subject too broad for me and frankly I'm out of my depth about those. I think you should try to break your concerns about all these issues down into separate questions and ask them on StackOverflow. For instance: ask a separate question about how to find out if a Postgres transaction is still active in Java with the EntityManager you are using, etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.