// Copyright 2017 JanusGraph Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.janusgraph.diskstorage.locking; import static org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLocker.LOCK_COL_END; import static org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLocker.LOCK_COL_START; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.junit.Assert.assertTrue; import java.time.Instant; import java.util.List; import org.janusgraph.diskstorage.BackendException; import org.janusgraph.diskstorage.util.*; import org.janusgraph.diskstorage.util.time.TimestampProviders; import org.easymock.EasyMock; import org.easymock.IMocksControl; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.janusgraph.diskstorage.Entry; import org.janusgraph.diskstorage.EntryList; import org.janusgraph.diskstorage.StaticBuffer; import org.janusgraph.diskstorage.keycolumnvalue.KeyColumnValueStore; import org.janusgraph.diskstorage.keycolumnvalue.KeySliceQuery; import org.janusgraph.diskstorage.keycolumnvalue.StoreTransaction; import org.janusgraph.diskstorage.locking.consistentkey.ConsistentKeyLockerSerializer; import org.janusgraph.diskstorage.locking.consistentkey.StandardLockCleanerRunnable; import org.janusgraph.diskstorage.util.BufferUtil; public class LockCleanerRunnableTest { private IMocksControl ctrl; private IMocksControl relaxedCtrl;; private StandardLockCleanerRunnable del; private KeyColumnValueStore store; private StoreTransaction tx; private final ConsistentKeyLockerSerializer codec = new ConsistentKeyLockerSerializer(); private final KeyColumn kc = new KeyColumn( new StaticArrayBuffer(new byte[]{(byte) 1}), new StaticArrayBuffer(new byte[]{(byte) 2})); private final StaticBuffer key = codec.toLockKey(kc.getKey(), kc.getColumn()); private final KeySliceQuery ksq = new KeySliceQuery(key, LOCK_COL_START, LOCK_COL_END); private final StaticBuffer defaultLockRid = new StaticArrayBuffer(new byte[]{(byte) 32}); @Before public void setupMocks() { relaxedCtrl = EasyMock.createControl(); tx = relaxedCtrl.createMock(StoreTransaction.class); ctrl = EasyMock.createStrictControl(); store = ctrl.createMock(KeyColumnValueStore.class); } @After public void verifyMocks() { ctrl.verify(); } /** * Simplest case test of the lock cleaner. */ @Test public void testDeleteSingleLock() throws BackendException { Instant now = Instant.ofEpochMilli(1L); Entry expiredLockCol = StaticArrayEntry.of(codec.toLockCol(now, defaultLockRid, TimestampProviders.MILLI), BufferUtil.getIntBuffer(0)); EntryList expiredSingleton = StaticArrayEntryList.of(expiredLockCol); now = now.plusMillis(1); del = new StandardLockCleanerRunnable(store, kc, tx, codec, now, TimestampProviders.MILLI); expect(store.getSlice(eq(ksq), eq(tx))) .andReturn(expiredSingleton); store.mutate( eq(key), eq(ImmutableList.<Entry> of()), eq(ImmutableList.<StaticBuffer> of(expiredLockCol.getColumn())), anyObject(StoreTransaction.class)); ctrl.replay(); del.run(); } /** * Test the cleaner against a set of locks where some locks have timestamps * before the cutoff and others have timestamps after the cutoff. One lock * has a timestamp equal to the cutoff. */ @Test public void testDeletionWithExpiredAndValidLocks() throws BackendException { final int lockCount = 10; final int expiredCount = 3; assertTrue(expiredCount + 2 <= lockCount); final long timeIncr = 1L; final Instant timeStart = Instant.EPOCH; final Instant timeCutoff = timeStart.plusMillis(expiredCount * timeIncr); ImmutableList.Builder<Entry> locksBuilder = ImmutableList.builder(); ImmutableList.Builder<Entry> delsBuilder = ImmutableList.builder(); for (int i = 0; i < lockCount; i++) { final Instant ts = timeStart.plusMillis(timeIncr * i); Entry lock = StaticArrayEntry.of( codec.toLockCol(ts, defaultLockRid, TimestampProviders.MILLI), BufferUtil.getIntBuffer(0)); if (ts.isBefore(timeCutoff)) { delsBuilder.add(lock); } locksBuilder.add(lock); } EntryList locks = StaticArrayEntryList.of(locksBuilder.build()); EntryList dels = StaticArrayEntryList.of(delsBuilder.build()); assertTrue(expiredCount == dels.size()); del = new StandardLockCleanerRunnable(store, kc, tx, codec, timeCutoff, TimestampProviders.MILLI); expect(store.getSlice(eq(ksq), eq(tx))).andReturn(locks); store.mutate( eq(key), eq(ImmutableList.<Entry> of()), eq(columnsOf(dels)), anyObject(StoreTransaction.class)); ctrl.replay(); del.run(); } /** * Locks with timestamps equal to or numerically greater than the cleaner * cutoff timestamp must be preserved. Test that the cleaner reads locks by * slicing the store and then does <b>not</b> attempt to write. */ @Test public void testPreservesLocksAtOrAfterCutoff() throws BackendException { final Instant cutoff = Instant.ofEpochMilli(10L); Entry currentLock = StaticArrayEntry.of(codec.toLockCol(cutoff, defaultLockRid, TimestampProviders.MILLI), BufferUtil.getIntBuffer(0)); Entry futureLock = StaticArrayEntry.of(codec.toLockCol(cutoff.plusMillis(1), defaultLockRid, TimestampProviders.MILLI), BufferUtil.getIntBuffer(0)); EntryList locks = StaticArrayEntryList.of(currentLock, futureLock); // Don't increment cutoff: lockCol is exactly at the cutoff timestamp del = new StandardLockCleanerRunnable(store, kc, tx, codec, cutoff, TimestampProviders.MILLI); expect(store.getSlice(eq(ksq), eq(tx))).andReturn(locks); ctrl.replay(); del.run(); } /** * Return a new list of {@link Entry#getColumn()} for each element in the * argument list. */ private static List<StaticBuffer> columnsOf(List<Entry> l) { return Lists.transform(l, new Function<Entry, StaticBuffer>() { @Override public StaticBuffer apply(Entry e) { return e.getColumn(); } }); } }