Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix LRU comparison algorithm #18624

Merged
merged 3 commits into from Jun 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -33,9 +33,10 @@ public class LRUEvictionPolicyComparator

@Override
public int compare(EvictableEntryView e1, EvictableEntryView e2) {
int result = Long.compare(e1.getLastAccessTime(), e2.getLastAccessTime());
// if access times are same, we try to select oldest entry to evict
return result == 0 ? Long.compare(e1.getCreationTime(), e2.getCreationTime()) : result;
long time1 = Math.max(e1.getCreationTime(), e1.getLastAccessTime());
long time2 = Math.max(e2.getCreationTime(), e2.getLastAccessTime());
Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these comparisons after the change in AbstractRecordStore? I don't know exactly where the last access times can be changed, so it's just a question to learn the cases that need them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparator is a common one used by multiple data structures, it is not specific to IMap, i have also fixed it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if we also apply the change made in AbstractRecordStore to AbstractCacheRecordStore (and all others) to have a convenience on these last access times.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no issue in the context of icache. Setting last-access-time is a missing requirement for imap after 4.2 improvements. No need for others now.


return Long.compare(time1, time2);
}

@Override
Expand Down
Expand Up @@ -142,6 +142,7 @@ public Record createRecord(Object value, long ttlMillis, long maxIdle, long now)
Record record = recordFactory.newRecord(value);
record.setCreationTime(now);
record.setLastUpdateTime(now);
record.setLastAccessTime(now);

updateStatsOnPut(false, now);
return record;
Expand Down
Expand Up @@ -125,20 +125,25 @@ public EvictionPolicyComparator getComparator() {
= new ArrayList<EvictionCandidate<Integer, CacheObjectRecord>>();

long baseTime = System.currentTimeMillis();

long minCreationTime = -1;
for (int i = 0; i < recordCount; i++) {
long creationTime = baseTime + (i * 100);
minCreationTime = minCreationTime == -1 ? creationTime : Math.min(creationTime, minCreationTime);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could also initialize minCreationTime = Long.MAX_VALUE and just use minCreationTime = Math.min(creationTime, minCreationTime); here instead of the ternary operator.


CacheObjectRecord record = new CacheObjectRecord(i, creationTime, Long.MAX_VALUE);
if (i == expectedEvictedRecordValue) {
// The record in the middle will be minimum access time.
// So, it will be selected for eviction
record.setLastAccessTime(baseTime - 1000);
// The record in the middle will be minimum access
// time. So, it will be selected for eviction
// (set creation-time also to keep this condition
// true --> creation-time <= last-access-time)
record.setCreationTime(minCreationTime);
record.setLastAccessTime(minCreationTime + 1);
} else if (i == expectedExpiredRecordValue) {
record.setExpirationTime(System.currentTimeMillis());
} else {
record.setLastAccessTime(creationTime + 1000);
}
records.add(new SimpleEvictionCandidate<Integer, CacheObjectRecord>(i, record));
records.add(new SimpleEvictionCandidate<>(i, record));
}

sleepAtLeastMillis(1);
Expand Down
@@ -0,0 +1,149 @@
/*
* Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved.
*
* 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 com.hazelcast.internal.eviction.impl.comparator;

import com.hazelcast.spi.eviction.EvictableEntryView;
import com.hazelcast.test.HazelcastSerialClassRunner;
import com.hazelcast.test.annotation.QuickTest;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import static org.junit.Assert.assertEquals;

@RunWith(HazelcastSerialClassRunner.class)
@Category(QuickTest.class)
public class LRUEvictionPolicyComparatorTest {

private static final long NOW = 20;

@Test
public void lru_comparator_does_not_prematurely_select_newly_created_entries() {
// 0. Entries to sort
List<TestEntryView> givenEntries = new LinkedList<>();
givenEntries.add(new TestEntryView(1, 1, 0));
givenEntries.add(new TestEntryView(2, 2, 3));
givenEntries.add(new TestEntryView(3, 2, 0));
givenEntries.add(new TestEntryView(4, 4, 4));
givenEntries.add(new TestEntryView(5, 5, 20));
givenEntries.add(new TestEntryView(6, 6, 6));
givenEntries.add(new TestEntryView(7, 7, 0));
givenEntries.add(new TestEntryView(8, 9, 15));
givenEntries.add(new TestEntryView(9, 10, 10));
givenEntries.add(new TestEntryView(10, 10, 0));

// 1. Create expected list of ordered elements by
// sorting entries based on their idle-times. Longest
// idle time must be the first element of the list.
List<TestEntryView> descOrderByIdleTimes = new LinkedList<>(givenEntries);
Collections.sort(descOrderByIdleTimes, (o1, o2) -> -Long.compare(idleTime(o1), idleTime(o2)));

// 2. Then sort given entries by using LRU eviction comparator.
Collections.sort(givenEntries, (o1, o2) -> LRUEvictionPolicyComparator.INSTANCE.compare(o1, o2));

// 3. Check both lists are equal
assertEquals(descOrderByIdleTimes, givenEntries);
}

private static long idleTime(EvictableEntryView entryView) {
return NOW - Math.max(entryView.getCreationTime(), entryView.getLastAccessTime());
}

private static class TestEntryView implements EvictableEntryView {
private long id;
private long creationTime;
private long lastAccessTime;

TestEntryView(long id, long creationTime, long lastAccessTime) {
this.id = id;
this.creationTime = creationTime;
this.lastAccessTime = lastAccessTime;
}

@Override
public long getCreationTime() {
return creationTime;
}

@Override
public long getLastAccessTime() {
return lastAccessTime;
}

public long getId() {
return id;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (o == null || getClass() != o.getClass()) {
return false;
}

TestEntryView that = (TestEntryView) o;

if (id != that.id) {
return false;
}
if (creationTime != that.creationTime) {
return false;
}
return lastAccessTime == that.lastAccessTime;
}

@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + (int) (creationTime ^ (creationTime >>> 32));
result = 31 * result + (int) (lastAccessTime ^ (lastAccessTime >>> 32));
return result;
}

@Override
public String toString() {
return "TestEntryView{"
+ "id=" + id
+ ", now=" + NOW
+ ", creationTime=" + creationTime
+ ", lastAccessTime=" + lastAccessTime
+ ", idleTime=" + idleTime(this) + '}';
}

@Override
public Object getKey() {
throw new UnsupportedOperationException();
}

@Override
public Object getValue() {
throw new UnsupportedOperationException();
}

@Override
public long getHits() {
throw new UnsupportedOperationException();
}
}
}
Expand Up @@ -344,10 +344,8 @@ public void testExpirationTime_whenMaxIdleTime_isSmallerThan_TTL() {
map.put(1, 1, 100, TimeUnit.SECONDS);

EntryView<Integer, Integer> entryView = map.getEntryView(1);
long lastAccessTime = entryView.getLastAccessTime();
long delayToExpiration = lastAccessTime + TimeUnit.SECONDS.toMillis(10);
long delayToExpiration = TimeUnit.SECONDS.toMillis(10);

// lastAccessTime is zero after put, we can find expiration by this calculation
long expectedExpirationTime = delayToExpiration + entryView.getCreationTime();
assertEquals(expectedExpirationTime, entryView.getExpirationTime());
}
Expand All @@ -367,14 +365,14 @@ public void testExpirationTime_whenMaxIdleTime_isBiggerThan_TTL() {
}

@Test
public void testLastAccessTime_isZero_afterFirstPut() {
public void testLastAccessTime_equals_creationTime_afterFirstPut() {
IMap<Integer, Integer> map = createMap();

map.put(1, 1);

EntryView<Integer, Integer> entryView = map.getEntryView(1);

assertEquals(0L, entryView.getLastAccessTime());
assertEquals(entryView.getCreationTime(), entryView.getLastAccessTime());
}

@Test
Expand Down Expand Up @@ -438,9 +436,9 @@ public void last_access_time_updated_on_primary_when_read_backup_data_enabled()

long lastAccessTimeBefore = map.getEntryView(1).getLastAccessTime();

sleepAtLeastMillis(10);
sleepAtLeastMillis(500);
map.get(1);
sleepAtLeastMillis(10);
sleepAtLeastMillis(500);
map.get(1);

long lastAccessTimeAfter = map.getEntryView(1).getLastAccessTime();
Expand Down