0

I have a class that is used to acquire and release locks for files. I use a customKey class that is just a ReentrantReadWriteLock with an id string (id being the file). For some reason, this only works in some situations, and in most it hangs on unlock of all things - my debugger traces it's use all the way there and then just gets stuck.

What am I doing wrong? I would get if a thread crashed and did not release it's lock but here a thread tries to call unlock and does not go any further.

This is the method for locking:

override fun acquire(lockId: String?, ownerId: String?, sequence: Long): Boolean
{
    if (lockId != null)
    {
        lockedList.find { customLock -> customLock.Id == lockId }.apply {
            if (this != null) //lock already exists for this ID
            {
                println("Locking file $lockId Existing lock")
                this.writeLock().lock()
                println("Locked file $lockId")
            } else //lock does not exist
            {
                val newLock = CustomLock(lockId)
                lockedList.add(newLock)
                println("Locking file $lockId")
                newLock.writeLock().lock()
                println("Locked file $lockId")
            }
        }
        return true
    } else
    {
        throw InvalidParameterException("ERROR: lockId or ownerId is null!")
    }
}

This is the method for releasing:

override fun release(lockId: String?, ownerId: String?)
    {
        if (lockId != null)
        {
            lockedList.find { customLock -> customLock.Id == lockId }.apply {
                if (this != null)
                {
                    println("Unlocking file $lockId")
                    this.writeLock().unlock()
                    if (this.isWriteLocked)
                    {
                        throw Exception("ERROR: Unlocking failed!")
                    }
                } else
                {
                    throw Exception("ERROR: Lock not found!")
                }
            }
        }
    }

Please do not bother to talk about architecture, this one is dictated by the assignment. Also please ignore the ownerId and sequence variables.

EDIT: I tried using just a single lock and while not very efficient, it did work, so @gidds may be onto something, but neither ConcurrentHashMap nor ConcurrentLinkedQueue (it is simpler to replace a List with) solved the problem.

EDIT2: This is the new class where I used the ConcurrentHashMap. It still does not work correctly, can anyone please point out where I messed up? Thanks

class LockServer(port: Int) : LockConnector, RemoteException()
{
private val lockedList = ConcurrentHashMap<String, CustomLock>()
private var registry: Registry = LocateRegistry.createRegistry(port)

init
{
    registry.bind(ServiceNames.LockService.toString(), UnicastRemoteObject.exportObject(this, port))
}

/**
 * Method acquire() should block the multiple calls from the clients for each specific lockId string.
 * It means when one client acquires the lock "A" and the "A" is not locked by any other clients,
 * the method should record the lock and return true. If the "A" is already locked by any other client,
 * the method is blocked and continues only after the lock "A" is released.
 * (Note: Return value false is not used in this basic implementation.
 * Parameters ownerId and sequence are also not used in this basic implementation.)
 */
override fun acquire(lockId: String?, ownerId: String?, sequence: Long): Boolean
{
    if (lockId != null)
    {
        lockedList.computeIfPresent(lockId){id, value ->
            println("Locking file $id Existing lock")
            value.writeLock().lock()
            println("Locked file $id")
            return@computeIfPresent value
        }
        lockedList.computeIfAbsent(lockId){
            val newLock = CustomLock(it)
            println("Locking file $lockId")
            newLock.writeLock().lock()
            println("Locked file $lockId")
            return@computeIfAbsent newLock
        }
        return true
    } else
    {
        throw InvalidParameterException("ERROR: lockId or ownerId is null!")
    }
}

/**
 * Method release() should release the lock and unblock all waiting acquire() calls for the same lock.
 * (Note: Parameter ownerId is not used in this basic implementation.)
 */
override fun release(lockId: String?, ownerId: String?)
{
    if (lockId != null)
    {
        lockedList.computeIfPresent(lockId){ id, value ->
            println("Unlocking file $id")
            value.writeLock().unlock()
            println("Unlocked file $id")
            return@computeIfPresent value
        }
    }
}

/**
 * Method stop() unbinds the current server object from the RMI registry and unexports it.
 */
override fun stop()
{
    registry.unbind(ServiceNames.LockService.toString())
}

}

EDIT3: New implementation for acquire:

lockedList.compute(lockId){id, value ->
            if (value == null)
            {
                println("Locking file $id")
                val newLock = CustomLock(id)
                newLock.writeLock().lock()
                println("Locked file $id")
                return@compute newLock
            }
            println("Locking file $id Existing lock")
            value.writeLock().lock()
            println("Locked file $id")
            return@compute value
        }

and for release:

println("Unlocking $lockId")
        lockedList[lockId]!!.writeLock().unlock()
        println("Unlocked $lockId")

Still the same failure

Luk164
  • 657
  • 8
  • 22
  • 2
    Unless you fix the fundamental check-then-act problem, just using a different class won’t help. So “it is simpler to replace a List with” is not a useful criteria for selecting the right data structure. – Holger Oct 06 '20 at 15:59
  • @Holger I added a new implementation in edit, can you take a look at it please? – Luk164 Oct 07 '20 at 15:16
  • 1
    What if someone added a lock right between `computeIfPresent` and `computeIfAbsent`? The literal meaning of “atomic” does already preclude having two operations. You can use a single `compute` call, checking the old value against `null` within the function or better, perform the equivalent of the Java code, `map.computeIfAbsent(lockId, CustomLock::new).writeLock().lock()` using the `computeIfAbsent` only to get the existing or create a new lock and perform the lock on the result. The unlocking is even simpler: `map.get(lockId).writeLock().unlock()`. This will and *should* throw for wrong IDs. – Holger Oct 07 '20 at 15:24
  • @Holger Thank you for your explanation. Check the edit for my changes. Still the same failure. – Luk164 Oct 07 '20 at 16:53
  • 1
    It’s unnecessarily redundant, as the statements after `val newLock = CustomLock(id)` are identical to what happens in the non-`null` case, but still, it’s now correctly avoiding races when locking. Since you say, you get deadlocks (or apparent deadlocks) on *unlock* actions, the problem must be connected to the implementation of `CustomLock`. As far as I know, the standard lock implementations do not block inside unlock at all. They may throw exceptions for mismatching unlock attempts, but not block. – Holger Oct 07 '20 at 16:59
  • @Holger CustomLock is just a ReentrantReadWriteLock with one extra variables. Lock is supposed to block the thread until it can be acquired. – Luk164 Oct 07 '20 at 17:16
  • 1
    Then, there seems to be no way around making a thread dump of a blocked thread, including the parts within the lock implementation, to get a hint about what’s going on. – Holger Oct 08 '20 at 06:48
  • @Holger I think Михаил Нафталь just nailed the problem, now I just need to find out how to avoid it. – Luk164 Oct 08 '20 at 08:56
  • 1
    That’s a classical deadlock scenario, but it means getting stuck at lock, not unlock. – Holger Oct 08 '20 at 09:10
  • Yeah, I am still not sure about that, but this actually did help - switching to a single lock made it all run smoothly, if not very efficiently. Maybe it can be explained by some inner mechanism of the lock itself? – Luk164 Oct 08 '20 at 09:22

2 Answers2

1

This may not be your problem, but the code has a race condition when adding a new lock: if two threads try to lock the same (new) file, they could both create a lock for it.  Both locks would get added to the list, but only the first would be found after that.  (This assumes that the list itself is thread-safe; otherwise one of the adds could fail, loop forever, or leave the list in an inconsistent state and crash later.)

You could fix this with some synchronisation.  But a better approach might be to store the locks in a ConcurrentHashMap (keyed on the lock ID) instead of a list, and use an atomic operation such as computeIfAbsent() to create a new lock safely.  (That would also improve asymptotic performance, as it would avoid scanning a list each time.)

Also, as a matter of style, the use of apply() on the lock looks a bit awkward.  (Its usual use is for customising a newly-created object.)  I think let() would be more idiomatic there; you'd just have to change this to it inside.  Or use an old-fashioned temporary variable, of course.

gidds
  • 16,558
  • 2
  • 19
  • 26
  • 1
    When this list is not thread safe, *all* adds can exhibit the mentioned behavior, not just one of them. – Holger Oct 06 '20 at 12:14
  • Nope, did not help. I also added a log for checking the amount of locks added but they never rose above the expected amount. – Luk164 Oct 06 '20 at 12:26
1

This may not be the problem of LockServer class, but the ones that are using it:

thread1:

acquire("file1")
acquire("file2")
release("file2")
release("file1")

thread2:

acquire("file2")
acquire("file1")
release("file1")
release("file2")

And it happened that the execution order was the following:

thread1.acquire("file1")
thread2.acquire("file2")
thread1.acquire("file2") //locked by thread2, waiting
thread2.acquire("file1") //locked by thread1... BOOM, deadlock!

UPD.:

Consider using tryLock() (possibly with some timeout) instead of simple lock() for existing locks:

    fun tryAcquire(lockId: String?, timeout: Long, unit: TimeUnit): Boolean {
        if (lockId != null) {
            var success = false
            lockedList.compute(lockId) { id, value ->
                if (value == null) {
                    println("Locking file $id")
                    val newLock = CustomLock(id)
                    newLock.writeLock().lock()
                    success = true
                    println("Locked file $id")
                    return@compute newLock
                }
                println("Locking file $id Existing lock")
                val lock = value.writeLock()
                if (lock.tryLock() || lock.tryLock(timeout, unit)) {
                    success = true
                    println("Locked file $id")
                }
                return@compute value
            }
            return success
        } else {
            throw InvalidParameterException("ERROR: lockId or ownerId is null!")
        }
    }
  • You know what? I think you just nailed it! But how can I avoid this? – Luk164 Oct 08 '20 at 08:55
  • This seems to have done it, though I also had to modify release a bit. For some reason an unlock is being called even when not actually locked. – Luk164 Oct 08 '20 at 12:01