diff --git a/drivers/mongock-driver-mongodb/mongodb-driver-test-template/src/main/java/io/mongock/driver/mongodb/test/template/MongoLockManagerITestBase.java b/drivers/mongock-driver-mongodb/mongodb-driver-test-template/src/main/java/io/mongock/driver/mongodb/test/template/MongoLockManagerITestBase.java index 90904debe..9de5d0f18 100644 --- a/drivers/mongock-driver-mongodb/mongodb-driver-test-template/src/main/java/io/mongock/driver/mongodb/test/template/MongoLockManagerITestBase.java +++ b/drivers/mongock-driver-mongodb/mongodb-driver-test-template/src/main/java/io/mongock/driver/mongodb/test/template/MongoLockManagerITestBase.java @@ -14,6 +14,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import java.util.Date; @@ -36,10 +37,13 @@ public abstract class MongoLockManagerITestBase extends IntegrationTestBase { public void setUp() { initializeRepository(); TimeService timeUtils = new TimeService(); - lockManager = new DefaultLockManager(repository, timeUtils) + lockManager = DefaultLockManager.builder() + .setLockRepository(repository) + .setTimeUtils( timeUtils) .setLockAcquiredForMillis(LOCK_ACQUIRED_FOR_MILLIS) - .setLockTryFrequencyMillis(LOCK_TRY_FRQUENCY_MILLIS) - .setLockQuitTryingAfterMillis(LOCK_QUIT_TRYING_AFTER_MILLIS); + .setLockQuitTryingAfterMillis(LOCK_TRY_FRQUENCY_MILLIS) + .setLockTryFrequencyMillis(LOCK_QUIT_TRYING_AFTER_MILLIS) + .build(); } @After diff --git a/mongock-core/mongock-driver/mongock-driver-api/src/main/java/io/mongock/driver/api/lock/LockManager.java b/mongock-core/mongock-driver/mongock-driver-api/src/main/java/io/mongock/driver/api/lock/LockManager.java index 3f7c6a996..a966df36d 100644 --- a/mongock-core/mongock-driver/mongock-driver-api/src/main/java/io/mongock/driver/api/lock/LockManager.java +++ b/mongock-core/mongock-driver/mongock-driver-api/src/main/java/io/mongock/driver/api/lock/LockManager.java @@ -40,37 +40,11 @@ default String getDefaultKey() { */ void releaseLockDefault(); - /** - *

If the flag 'waitForLog' is set, indicates the maximum time it will wait for the lock in total.

- * - * @param millis max waiting time for lock. Must be greater than 0 - * @return LockChecker object for fluent interface - */ - LockManager setLockQuitTryingAfterMillis(long millis); - /** * @return lock try frequency */ long getLockTryFrequency(); - /** - *

Updates the maximum number of tries to acquire the lock, if the flag 'waitForLog' is set

- *

Default 1

- * - * @param millis number of tries - * @return LockChecker object for fluent interface - */ - LockManager setLockTryFrequencyMillis(long millis); - - /** - *

Indicates the number of milliseconds the lock will be acquired for

- *

Default 3 minutes

- * - * @param lockAcquiredForMillis milliseconds the lock will be acquired for - * @return LockChecker object for fluent interface - */ - LockManager setLockAcquiredForMillis(long lockAcquiredForMillis); - /** * @return Lock's owner */ @@ -86,6 +60,7 @@ default String getDefaultKey() { */ void clean(); + long getMillisUntilRefreshRequired(); @Override void close(); diff --git a/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/driver/ConnectionDriverBase.java b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/driver/ConnectionDriverBase.java index d21f590ac..e0a22c0b2 100644 --- a/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/driver/ConnectionDriverBase.java +++ b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/driver/ConnectionDriverBase.java @@ -40,10 +40,12 @@ public final void initialize() { LockRepository lockRepository = this.getLockRepository(); lockRepository.setIndexCreation(isIndexCreation()); lockRepository.initialize(); - lockManager = new DefaultLockManager(lockRepository, TIME_SERVICE) + lockManager = DefaultLockManager.builder() + .setLockRepository(lockRepository) .setLockAcquiredForMillis(lockAcquiredForMillis) .setLockQuitTryingAfterMillis(lockQuitTryingAfterMillis) - .setLockTryFrequencyMillis(lockTryFrequencyMillis); + .setLockTryFrequencyMillis(lockTryFrequencyMillis) + .build(); ChangeEntryService changeEntryService = getChangeEntryService(); changeEntryService.setIndexCreation(isIndexCreation()); changeEntryService.initialize(); diff --git a/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/DefaultLockManager.java b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/DefaultLockManager.java index 983317b88..cdc9f8bef 100644 --- a/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/DefaultLockManager.java +++ b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/DefaultLockManager.java @@ -15,8 +15,6 @@ *

This class is responsible of managing the lock at high level. It provides 3 main methods which are * for acquiring, ensuring(doesn't acquires the lock, just refresh the expiration time if required) and * releasing the lock.

- *

Implementation note: This class is not thread safe. If in future development thread safety is needed, please consider - * using volatile for expiresAt field and synchronized mechanism.

*/ @NotThreadSafe public class DefaultLockManager implements LockManager { @@ -51,35 +49,43 @@ public class DefaultLockManager implements LockManager { private final String owner; /** - *

The period of time for which the lock will be owned.

+ * Daemon that will ensure the lock in background. */ - private long lockAcquiredForMillis = DEFAULT_LOCK_ACQUIRED_FOR_MILLIS; + private LockDaemon lockDaemon; /** - *

Milliseconds after which it will try to acquire the lock again

+ *

The period of time for which the lock will be owned.

*/ - private long lockTryFrequencyMillis = DEFAULT_TRY_FREQUENCY_MILLIS; + private final long lockAcquiredForMillis; /** - *

Maximum time it will wait for the lock in total.

+ *

Milliseconds after which it will try to acquire the lock again

*/ - private long lockQuitTryingAfterMillis = DEFAULT_QUIT_TRY_AFTER_MILLIS; + private final long lockTryFrequencyMillis; /** *

The margin in which the lock should be refresh to avoid losing it

*/ - private long lockRefreshMarginMillis = DEFAULT_LOCK_REFRESH_MARGIN_MILLIS; + private final long lockRefreshMarginMillis; + /** + *

Maximum time it will wait for the lock in total.

+ */ + private final long lockQuitTryingAfterMillis; /** * Moment when will mandatory to acquire the lock again. */ - private Date lockExpiresAt = null; + private volatile Date lockExpiresAt = null; /** * The instant the acquisition has shouldStopTryingAt */ - private Instant shouldStopTryingAt; + private volatile Instant shouldStopTryingAt; + + public static DefaultLockManagerBuilder builder() { + return new DefaultLockManagerBuilder(); + } /** * Constructor takes some bean injections @@ -88,9 +94,18 @@ public class DefaultLockManager implements LockManager { * @param timeUtils time utils service */ //TODO add lock configuration to constructor, make fields finals and move DEFAULTS away - public DefaultLockManager(LockRepository repository, TimeService timeUtils) { + public DefaultLockManager(LockRepository repository, + TimeService timeUtils, + long lockAcquiredForMillis, + long lockQuitTryingAfterMillis, + long lockTryFrequencyMillis, + long lockRefreshMarginMillis) { this.repository = repository; this.timeUtils = timeUtils; + this.lockAcquiredForMillis = lockAcquiredForMillis; + this.lockQuitTryingAfterMillis = lockQuitTryingAfterMillis; + this.lockTryFrequencyMillis = lockTryFrequencyMillis; + this.lockRefreshMarginMillis = lockRefreshMarginMillis; this.owner = UUID.randomUUID().toString();//TODO reconsider this } @@ -106,8 +121,7 @@ public void acquireLockDefault() throws LockCheckException { acquireLock(getDefaultKey()); } - private void acquireLock(String lockKey) { - startAcquisitionTimer(); + private void acquireLock(String lockKey) throws LockCheckException { boolean keepLooping = true; do { try { @@ -116,6 +130,7 @@ private void acquireLock(String lockKey) { repository.insertUpdate(new LockEntry(lockKey, LockStatus.LOCK_HELD.name(), owner, newLockExpiresAt)); logger.info("Mongock acquired the lock until: {}", newLockExpiresAt); updateStatus(newLockExpiresAt); + lockDaemon.activate(); keepLooping = false; } catch (LockPersistenceException ex) { handleLockException(true, ex); @@ -134,8 +149,7 @@ public void ensureLockDefault() throws LockCheckException { ensureLock(getDefaultKey()); } - private void ensureLock(String lockKey) { - startAcquisitionTimer(); + private void ensureLock(String lockKey) throws LockCheckException { boolean keepLooping = true; do { if (needsRefreshLock()) { @@ -146,6 +160,7 @@ private void ensureLock(String lockKey) { repository.updateIfSameOwner(lockEntry); updateStatus(lockExpiresAtTemp); logger.info("Mongock refreshed the lock until: {}", lockExpiresAtTemp); + lockDaemon.activate(); keepLooping = false; } catch (LockPersistenceException ex) { handleLockException(false, ex); @@ -167,7 +182,10 @@ public void releaseLockDefault() { } /** - * release the default lock. Useful in try/catch blocks + * Required when lock managing is done. + * Responsible for: + * - Release the lock in database (not critical. It will be expired eventually ) + * - Cancel the lock daemon (CRITICAL. Otherwise it will keep refreshing the lock making other Mongock process starved) */ @Override public void close() { @@ -175,6 +193,9 @@ public void close() { } private void releaseLock(String lockKey) { + if (lockDaemon != null) { + lockDaemon.cancel(); + } if (this.lockExpiresAt == null) { return; } @@ -185,53 +206,13 @@ private void releaseLock(String lockKey) { logger.info("Mongock released the lock"); } - /** - *

If the flag 'waitForLog' is set, indicates the maximum time it will wait for the lock in each try.

- *

Default 3 minutes

- * - * @param millis max waiting time for lock. Must be greater than 0 - * @return LockChecker object for fluent interface - */ - public DefaultLockManager setLockQuitTryingAfterMillis(long millis) { - if (millis <= 0) { - throw new IllegalArgumentException("Lock-quit-trying-after must be grater than 0 "); - } - lockQuitTryingAfterMillis = millis; - return this; - } @Override public long getLockTryFrequency() { return lockTryFrequencyMillis; } - public DefaultLockManager setLockTryFrequencyMillis(long millis) { - if (millis < MINIMUM_WAITING_TO_TRY_AGAIN) { - throw new IllegalArgumentException(String.format("Lock-try-frequency must be grater than %d", MINIMUM_WAITING_TO_TRY_AGAIN)); - } - lockTryFrequencyMillis = millis; - return this; - } - - /** - *

Indicates the number of milliseconds the lock will be acquired for

- *

Minimum 3 seconds

- * - * @param millis milliseconds the lock will be acquired for - * @return LockChecker object for fluent interface - */ - public DefaultLockManager setLockAcquiredForMillis(long millis) { - if (millis < MIN_LOCK_ACQUIRED_FOR_MILLIS) { - throw new IllegalArgumentException(String.format(EXPIRATION_ARG_ERROR_MSG, MIN_LOCK_ACQUIRED_FOR_MILLIS)); - } - lockAcquiredForMillis = millis; - long marginTemp = (long) (lockAcquiredForMillis * LOCK_REFRESH_MARGIN_PERCENTAGE); - lockRefreshMarginMillis = marginTemp > MIN_LOCK_REFRESH_MARGIN_MILLIS ? marginTemp : MIN_LOCK_REFRESH_MARGIN_MILLIS; - return this; - } - private void handleLockException(boolean acquiringLock, LockPersistenceException ex) { - LockEntry currentLock = repository.findByKey(getDefaultKey()); if (isAcquisitionTimerOver()) { @@ -300,6 +281,22 @@ public void clean() { repository.deleteAll(); } + @Override + public long getMillisUntilRefreshRequired() { + + + if (lockExpiresAt != null) { + return lockExpiresAt.getTime() - timeUtils.currentTime().getTime() - lockRefreshMarginMillis; + } else { + return lockAcquiredForMillis - lockRefreshMarginMillis; + } + +// return lockExpiresAt != null +// ? lockExpiresAt.getTime() - timeUtils.currentTime().getTime() - lockRefreshMarginMillis +// : lockAcquiredForMillis - lockRefreshMarginMillis; + + } + private boolean needsRefreshLock() { if (this.lockExpiresAt == null) { @@ -316,18 +313,24 @@ private void updateStatus(Date lockExpiresAt) { } /** - * idempotent operation to start the acquisition timer + * idempotent operation that + * - Starts the acquisition timer + * - Initializes and run the lock daemon. */ - private synchronized void startAcquisitionTimer() { + protected void initialize() { if (shouldStopTryingAt == null) { shouldStopTryingAt = timeUtils.nowPlusMillis(lockQuitTryingAfterMillis); } + if (lockDaemon == null) { + lockDaemon = new LockDaemon(this); + lockDaemon.start(); + } } /** * idempotent operation to start the acquisition timer */ - private synchronized void finishAcquisitionTimer() { + private void finishAcquisitionTimer() { shouldStopTryingAt = null; } @@ -336,4 +339,87 @@ private boolean isAcquisitionTimerOver() { return timeUtils.isPast(shouldStopTryingAt); } + + public static class DefaultLockManagerBuilder { + + private LockRepository lockRepository; + private long lockAcquiredForMillis = DEFAULT_LOCK_ACQUIRED_FOR_MILLIS; + private long lockTryFrequencyMillis = DEFAULT_TRY_FREQUENCY_MILLIS; + private long lockRefreshMarginMillis = DEFAULT_LOCK_REFRESH_MARGIN_MILLIS; + private long lockQuitTryingAfterMillis = DEFAULT_QUIT_TRY_AFTER_MILLIS; + private TimeService timeService = new TimeService(); + + public DefaultLockManagerBuilder() { + } + + /** + *

If the flag 'waitForLog' is set, indicates the maximum time it will wait for the lock in total.

+ * + * @param millis max waiting time for lock. Must be greater than 0 + * @return LockChecker object for fluent interface + */ + public DefaultLockManagerBuilder setLockQuitTryingAfterMillis(long millis) { + if (millis <= 0) { + throw new IllegalArgumentException("Lock-quit-trying-after must be grater than 0 "); + } + lockQuitTryingAfterMillis = millis; + return this; + } + + /** + *

Updates the maximum number of tries to acquire the lock, if the flag 'waitForLog' is set

+ *

Default 1

+ * + * @param millis number of tries + * @return LockChecker object for fluent interface + */ + public DefaultLockManagerBuilder setLockTryFrequencyMillis(long millis) { + if (millis < MINIMUM_WAITING_TO_TRY_AGAIN) { + throw new IllegalArgumentException(String.format("Lock-try-frequency must be grater than %d", MINIMUM_WAITING_TO_TRY_AGAIN)); + } + lockTryFrequencyMillis = millis; + return this; + } + + /** + *

Indicates the number of milliseconds the lock will be acquired for

+ *

Minimum 3 seconds

+ * + * @param millis milliseconds the lock will be acquired for + * @return LockChecker object for fluent interface + */ + public DefaultLockManagerBuilder setLockAcquiredForMillis(long millis) { + if (millis < MIN_LOCK_ACQUIRED_FOR_MILLIS) { + throw new IllegalArgumentException(String.format(EXPIRATION_ARG_ERROR_MSG, MIN_LOCK_ACQUIRED_FOR_MILLIS)); + } + lockAcquiredForMillis = millis; + long marginTemp = (long) (lockAcquiredForMillis * LOCK_REFRESH_MARGIN_PERCENTAGE); + lockRefreshMarginMillis = Math.max(marginTemp, MIN_LOCK_REFRESH_MARGIN_MILLIS); + return this; + } + + public DefaultLockManagerBuilder setLockRepository(LockRepository lockRepository) { + this.lockRepository = lockRepository; + return this; + } + + public DefaultLockManagerBuilder setTimeUtils(TimeService timeService) { + this.timeService = timeService; + return this; + } + + public DefaultLockManager build() { + DefaultLockManager lockManager = new DefaultLockManager( + lockRepository, + timeService, + lockAcquiredForMillis, + lockQuitTryingAfterMillis, + lockTryFrequencyMillis, + lockRefreshMarginMillis); + lockManager.initialize(); + return lockManager; + } + + + } } diff --git a/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/LockDaemon.java b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/LockDaemon.java new file mode 100644 index 000000000..cc55c1844 --- /dev/null +++ b/mongock-core/mongock-driver/mongock-driver-core/src/main/java/io/mongock/driver/core/lock/LockDaemon.java @@ -0,0 +1,59 @@ +package io.mongock.driver.core.lock; + +import io.mongock.driver.api.lock.LockManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LockDaemon extends Thread { + + private static final Logger logger = LoggerFactory.getLogger(LockDaemon.class); + private final LockManager lockManager; + private volatile boolean cancelled = false; + private volatile boolean active = false; + + public LockDaemon(LockManager lockManager) { + setName("mongock-lock-daemon-" + getId()); + this.lockManager = lockManager; + setDaemon(true); + } + + @Override + public void run() { + logger.info("Starting mongock lock daemon..."); + while(!cancelled) { + try { + if(active) { + logger.debug("Mongock lock daemon ensuring lock"); + lockManager.ensureLockDefault(); + } else { + logger.debug("Mongock lock daemon in loop but not ensuring lock because it's been activated yet"); + } + } catch(Exception ex) { + logger.error("Error ensuring the lock at the lock daemon", ex); + cancel(); + break; + } + repose(lockManager.getMillisUntilRefreshRequired()); + } + logger.info("Cancelled mongock lock daemon"); + } + + private void repose(long timeForResting) { + try { + logger.debug("Mongock lock daemon going to sleep: " + timeForResting + "ms"); + sleep(timeForResting); + } catch (InterruptedException ex) { + logger.warn("Interrupted exception ignored"); + } + } + + public void cancel() { + logger.info("Cancelling mongock lock daemon..."); + cancelled = true; + } + + public void activate() { + active = true; + } + +} diff --git a/mongock-core/mongock-driver/mongock-driver-core/src/test/java/io/mongock/driver/core/lock/DefaultLockManagerTest.java b/mongock-core/mongock-driver/mongock-driver-core/src/test/java/io/mongock/driver/core/lock/DefaultLockManagerTest.java index ccbb87720..61d5be877 100644 --- a/mongock-core/mongock-driver/mongock-driver-core/src/test/java/io/mongock/driver/core/lock/DefaultLockManagerTest.java +++ b/mongock-core/mongock-driver/mongock-driver-core/src/test/java/io/mongock/driver/core/lock/DefaultLockManagerTest.java @@ -3,10 +3,13 @@ import io.mongock.driver.api.lock.LockCheckException; import io.mongock.driver.api.lock.LockManager; import io.mongock.utils.TimeService; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.internal.verification.AtLeast; +import org.mockito.internal.verification.AtMost; import org.mockito.internal.verification.Times; import java.time.Instant; @@ -43,19 +46,80 @@ private static void assertExceptionMessage(LockCheckException ex) { assertTrue(ex.getMessage().contains("Quit trying lock after 180000 millis due to LockPersistenceException:")); } + @Before public void setUp() { - lockManager = new DefaultLockManager(lockRepository = Mockito.mock(LockRepositoryWithEntity.class), timeUtils = Mockito.mock(TimeService.class)) + lockRepository = Mockito.mock(LockRepositoryWithEntity.class); + timeUtils = Mockito.mock(TimeService.class); + when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); + } + + private void setLockManager(long lockActiveMillis, long quitTryingAfterMillis, long tryFrequency) { + lockManager = DefaultLockManager.builder() + .setLockRepository(lockRepository) + .setTimeUtils(timeUtils) .setLockAcquiredForMillis(lockActiveMillis) .setLockQuitTryingAfterMillis(quitTryingAfterMillis) - .setLockTryFrequencyMillis(tryFrequency); + .setLockTryFrequencyMillis(tryFrequency) + .build(); + } + + @After + public void tearDown() { + if (lockManager != null) { + lockManager.close(); + } + } + + @Test + public void shouldRefresh_InBackground_IfNotExplicitCall() throws InterruptedException { + when(timeUtils.currentTime()).thenReturn(new Date(40000L));// Exactly the expiration time(minus margin) + + DefaultLockManager lockManager = new DefaultLockManager( + lockRepository, + timeUtils, + 3000L, + quitTryingAfterMillis, + tryFrequency, + 1000L); + + DefaultLockManager lockManagerSpy = Mockito.spy(lockManager); + lockManagerSpy.initialize(); + lockManagerSpy.acquireLockDefault(); + Thread.sleep(7000L); + + verify(lockManagerSpy, new AtLeast(1)).ensureLockDefault(); + verify(lockManagerSpy, new AtMost(5)).ensureLockDefault(); + } + + @Test + public void shouldRefresh_InBackground_AfterManagerIsClosed() throws InterruptedException { + when(timeUtils.currentTime()).thenReturn(new Date(40000L));// Exactly the expiration time(minus margin) + + DefaultLockManager lockManager = new DefaultLockManager( + lockRepository, + timeUtils, + 3000L, + quitTryingAfterMillis, + tryFrequency, + 2000L); + + DefaultLockManager lockManagerSpy = Mockito.spy(lockManager); + lockManagerSpy.initialize(); + lockManagerSpy.acquireLockDefault(); + lockManagerSpy.close(); + Thread.sleep(7000L); + + verify(lockManagerSpy, new Times(0)).ensureLockDefault(); } + @Test public void shouldRetrieveLock_WhenAcquireLock() throws LockPersistenceException, LockCheckException { //given Date expirationAt = new Date(1000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); // when lockManager.acquireLockDefault(); @@ -66,11 +130,13 @@ public void shouldRetrieveLock_WhenAcquireLock() throws LockPersistenceException @Test public void shouldAlwaysAskForLock_WhenAcquireLock_RegardlessIfItIsAlreadyAcquired() throws LockPersistenceException, LockCheckException { + //given Date expirationAt = new Date(1000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); when(timeUtils.currentTime()).thenReturn(new Date(40000L));// Exactly the expiration time(minus margin) when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); lockManager.acquireLockDefault(); //when @@ -86,6 +152,7 @@ public void shouldAlwaysAskForLock_WhenAcquireLock_RegardlessIfItIsExpired() thr Date expirationAt = new Date(1000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); when(timeUtils.currentTime()).thenReturn(new Date(39999L));// 1ms less than the expiration time(minus margin) + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); lockManager.acquireLockDefault(); // when @@ -104,10 +171,11 @@ public void shouldWaitMinimum_IfWaitingTimeIsLessThanMinimum_WhenAcquireLock() t .doNothing() .when(lockRepository).insertUpdate(any(LockEntry.class)); - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); Date newExpirationAt = new Date(1000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); when(timeUtils.currentTime()).thenReturn(new Date(expiresAt - waitingTime)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); //when long timeBeforeCall = System.currentTimeMillis(); @@ -122,20 +190,21 @@ public void shouldWaitMinimum_IfWaitingTimeIsLessThanMinimum_WhenAcquireLock() t @Test public void shouldWaitUntilExpiration_IfFrequencyIsHigherThanExpiration_WhenAcquireLock() throws LockPersistenceException, LockCheckException { //given - doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")) - .doNothing() - .when(lockRepository).insertUpdate(any(LockEntry.class)); long expiresAt = 3000L; long currentMoment = 2000L; long waitingTime = expiresAt - currentMoment; - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(DONT_CARE_DATE); when(timeUtils.currentTime()).thenReturn(new Date(currentMoment)); //when long instantBefore = System.currentTimeMillis(); - lockManager.setLockTryFrequencyMillis(3000L); + setLockManager(lockActiveMillis, quitTryingAfterMillis, 3000L); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); + doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")) + .doNothing() + .when(lockRepository).insertUpdate(any(LockEntry.class)); + lockManager.acquireLockDefault(); long spentMillis = System.currentTimeMillis() - instantBefore; @@ -154,13 +223,13 @@ public void shouldWaitFrequency_IfFrequencyIsLowerThanExpiration_WhenAcquireLock long expiresAt = 3000L; long currentMoment = 2000L; long waitingTime = 750L; - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(DONT_CARE_DATE); when(timeUtils.currentTime()).thenReturn(new Date(currentMoment)); //when long instantBefore = System.currentTimeMillis(); - lockManager.setLockTryFrequencyMillis(waitingTime); + setLockManager(lockActiveMillis, quitTryingAfterMillis, waitingTime); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); lockManager.acquireLockDefault(); long spentMillis = System.currentTimeMillis() - instantBefore; @@ -172,18 +241,18 @@ public void shouldWaitFrequency_IfFrequencyIsLowerThanExpiration_WhenAcquireLock @Test public void shouldNotWaitAndThrowException_IfWaitingTimeIsOver_WhenAcquireLock() throws LockPersistenceException, LockCheckException { //given - lockManager.setLockQuitTryingAfterMillis(1000L); doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")) .doNothing() .when(lockRepository).insertUpdate(any(LockEntry.class)); - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(DONT_CARE_LONG)); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(FAR_FUTURE_DATE); when(timeUtils.currentTime()).thenReturn(new Date(2000L)); when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); when(timeUtils.isPast(any(Instant.class))).thenReturn(true); + setLockManager(lockActiveMillis, 1000L, tryFrequency); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(DONT_CARE_LONG)); //when long timeBeforeCall = System.currentTimeMillis(); @@ -209,6 +278,7 @@ public void shouldNotWaiAndTryAgainStraightAway_IfWriteLockThrowsExceptionButItI .doNothing() .when(lockRepository).insertUpdate(any(LockEntry.class)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, waitingTime); when(lockRepository.findByKey(anyString())).thenReturn(new LockEntry( lockManager.getDefaultKey(), LockStatus.LOCK_HELD.name(), @@ -239,12 +309,12 @@ public void shouldThrowException_IfQuitTryingAfterReached_WhenAcquire() long waitingTime = quitTryingAfterMillis + 1; int invocationTimes = 1; + setLockManager(lockActiveMillis, quitTryingAfterMillis, waitingTime); doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).when(lockRepository).insertUpdate(any(LockEntry.class)); when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); Date newExpirationAt = new Date(100000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); when(timeUtils.currentTime()).thenReturn(new Date(expiresAt - waitingTime)); - when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); when(timeUtils.isPast(any(Instant.class))).thenReturn(true); //when @@ -271,12 +341,13 @@ public void shouldKeepTryingToAcquireLock_WhileQuitTryingAfterNotReached_WhenAcq doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).when(lockRepository).insertUpdate(any(LockEntry.class)); - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); Date newExpirationAt = new Date(100000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); when(timeUtils.currentTime()).thenReturn(new Date(expiresAt - waitingTime)); when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); doReturn(false, false, true).when(timeUtils).isPast(any(Instant.class)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); // when try { @@ -295,6 +366,7 @@ public void shouldCallRepository_WhenEnsureLock() throws LockPersistenceExceptio //given Date expirationAt = new Date(1000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); // when lockManager.ensureLockDefault(); @@ -310,7 +382,7 @@ public void shouldRefreshLock_IfLockIsExpired_whenEnsureLock() throws LockPersis when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); when(timeUtils.currentTime()).thenReturn(new Date(40000L));// Exactly the expiration time(minus margin) - lockManager.setLockAcquiredForMillis(3 * 1000L);// 3 seconds. Margin should 1 second + setLockManager(3 * 1000L, quitTryingAfterMillis, tryFrequency);// 3 seconds. Margin should 1 second lockManager.acquireLockDefault(); // when @@ -328,7 +400,7 @@ public void shouldNotRefreshLock_IfAlreadyAcquired_WhenEnsureLock() throws LockP when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(expirationAt); when(timeUtils.currentTime()).thenReturn(new Date(39999L));// 1ms less than the expiration time(minus margin) - lockManager.setLockAcquiredForMillis(3 * 1000L);// 3 seconds. Margin should 1 second + setLockManager(3 * 1000L, quitTryingAfterMillis, tryFrequency);// 3 seconds. Margin should 1 second lockManager.acquireLockDefault(); // when @@ -344,16 +416,17 @@ public void shouldNotWaitAndThrowException_IfLockHeldByOtherProcess_IfEnsureLock long expiresAt = 3000L; long waitingTime = 1000L; - doNothing().when(lockRepository).insertUpdate(any(LockEntry.class)); - doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).doNothing().when(lockRepository).updateIfSameOwner(any(LockEntry.class)); - - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); Date newExpirationAt = new Date(100000L); when(timeUtils.currentDatePlusMillis(anyLong())) .thenReturn(newExpirationAt); when(timeUtils.currentTime()) .thenReturn(new Date(40001L)) .thenReturn(new Date(expiresAt - waitingTime)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithOtherOwner(expiresAt)); + doNothing().when(lockRepository).insertUpdate(any(LockEntry.class)); + doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).doNothing().when(lockRepository).updateIfSameOwner(any(LockEntry.class)); + // when lockManager.ensureLockDefault(); @@ -365,6 +438,12 @@ public void shouldTryAgain_IfNeedsRefresh_whenEnsureLock() throws LockPersistenc //given long expiresAt = 3000L; long waitingTime = 1000L; + Date newExpirationAt = new Date(40000L); + when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); + when(timeUtils.currentTime()) + .thenReturn(new Date(40000L)) + .thenReturn(new Date(expiresAt - waitingTime)); + setLockManager(3 * 1000L, quitTryingAfterMillis, tryFrequency);// 3 seconds. Margin should 1 second doNothing().when(lockRepository).insertUpdate(any(LockEntry.class)); doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).doNothing() .when(lockRepository).updateIfSameOwner(any(LockEntry.class)); @@ -372,13 +451,6 @@ public void shouldTryAgain_IfNeedsRefresh_whenEnsureLock() throws LockPersistenc when(lockRepository.findByKey(anyString())) .thenReturn(new LockEntry(lockManager.getDefaultKey(), LockStatus.LOCK_HELD.name(), lockManager.getOwner(), new Date(expiresAt))); - Date newExpirationAt = new Date(40000L); - when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); - when(timeUtils.currentTime()) - .thenReturn(new Date(40000L)) - .thenReturn(new Date(expiresAt - waitingTime)); - lockManager.setLockAcquiredForMillis(3 * 1000L);// 3 seconds. Margin should 1 second - lockManager.acquireLockDefault(); // when @@ -396,8 +468,7 @@ public void shouldStopTrying_ifQuitTryingIsOver_WhenEnsureLock() throws LockPers //given long expiresAt = 3000L; long waitingTime = 1; - doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).when(lockRepository).updateIfSameOwner(any(LockEntry.class)); - when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithSameOwner(expiresAt)); + Date newExpirationAt = new Date(100000L); when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(newExpirationAt); when(timeUtils.currentTime()).thenReturn(new Date(expiresAt - waitingTime)); @@ -405,6 +476,9 @@ public void shouldStopTrying_ifQuitTryingIsOver_WhenEnsureLock() throws LockPers when(timeUtils.nowPlusMillis(anyLong())).thenReturn(DONT_CARE_INSTANT); doReturn(false, false, true) .when(timeUtils).isPast(any(Instant.class)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); + doThrow(new LockPersistenceException("acquireLockQuery", "newLockEntity", "dbErrorDetail")).when(lockRepository).updateIfSameOwner(any(LockEntry.class)); + when(lockRepository.findByKey(anyString())).thenReturn(createFakeLockWithSameOwner(expiresAt)); // when try { @@ -422,6 +496,7 @@ public void shouldCallRepository_ifLockHeld_WhenReleaseLock() { //given when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(new Date(1000000L)); when(timeUtils.currentTime()).thenReturn(new Date(0L)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); //when lockManager.acquireLockDefault(); @@ -436,6 +511,7 @@ public void shouldWriteTheLockInDB_IfLockIsReleased_WhenAcquireLock() throws Loc //given when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(new Date(1000000L)); when(timeUtils.currentTime()).thenReturn(new Date(0L)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); //when lockManager.acquireLockDefault(); @@ -452,6 +528,7 @@ public void shouldWriteTheLockInDB_IfLockIsReleased_WhenEnsureLock() throws Lock //given when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(new Date(1000000L)); when(timeUtils.currentTime()).thenReturn(new Date(0L)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); //when lockManager.acquireLockDefault(); @@ -466,7 +543,7 @@ public void shouldWriteTheLockInDB_IfLockIsReleased_WhenEnsureLock() throws Lock @Test public void shouldReturnSameValue_IfSetValue_WhenGetFrequency() { //given - lockManager.setLockTryFrequencyMillis(3000L); + setLockManager(lockActiveMillis, quitTryingAfterMillis, 3000L); //then assertEquals(3000L, lockManager.getLockTryFrequency()); @@ -474,22 +551,25 @@ public void shouldReturnSameValue_IfSetValue_WhenGetFrequency() { @Test(expected = IllegalArgumentException.class) public void shouldThrowIllegalException_WhenFrequencyIsLessOrEqualMinimum() { - lockManager.setLockTryFrequencyMillis(499L); + + setLockManager(lockActiveMillis, quitTryingAfterMillis, 499L); } @Test(expected = IllegalArgumentException.class) public void shouldThrowIllegalException_WhenQuitTryingIsLessOrEqualZero() { - lockManager.setLockQuitTryingAfterMillis(0); + setLockManager(lockActiveMillis, 0, tryFrequency); } @Test(expected = IllegalArgumentException.class) public void shouldThrowIllegalException_WhenAcquiredForLessThanMinimum() { - lockManager.setLockAcquiredForMillis(2999L); + + setLockManager(2999L, quitTryingAfterMillis, tryFrequency); } @Test public void shouldReturnFalse_IfNotStarted_WhenIsLockHeld() { //when + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); boolean lockHeld = lockManager.isLockHeld(); //then @@ -501,6 +581,7 @@ public void shouldReturnTrue_IfStarted_WhenIsLockHeld() throws LockCheckExceptio //given when(timeUtils.currentDatePlusMillis(anyLong())).thenReturn(new Date(1000000L)); when(timeUtils.currentTime()).thenReturn(new Date(0L)); + setLockManager(lockActiveMillis, quitTryingAfterMillis, tryFrequency); lockManager.acquireLockDefault(); //when