/* * ModeShape (http://www.modeshape.org) * * 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.modeshape.jcr.journal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.modeshape.common.FixFor; import org.modeshape.common.util.FileUtil; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.change.Change; import org.modeshape.jcr.cache.change.ChangeSet; import org.modeshape.jcr.value.BinaryKey; import org.modeshape.jcr.value.basic.ModeShapeDateTime; /** * Unit test for {@link LocalJournal} * * @author Horia Chiorean (hchiorea@redhat.com) */ public class LocalJournalTest { protected ChangeJournal journal; private DateTime timestamp1; private DateTime timestamp2; private DateTime timestamp3; @Before public void before() throws Exception { FileUtil.delete("target/local_journal"); this.journal = journal(); journal.start(); insertTestRecords(); } @After public void after() { journal.shutdown(); } protected void insertTestRecords() throws InterruptedException { // p1 has 4 changesets (sleep for 1 ms between them to make sure the timestamps are different ChangeSet process1Changes1 = TestChangeSet.create("j1", 5); journal.notify(process1Changes1); ChangeSet process1Changes2 = TestChangeSet.create("j1", 1); journal.notify(process1Changes2); ChangeSet process1Changes3 = TestChangeSet.create("j1", 1); journal.notify(process1Changes3); ChangeSet process1Changes4 = TestChangeSet.create("j1", 1); journal.notify(process1Changes4); timestamp1 = new ModeShapeDateTime(process1Changes4.getTimestamp().getMilliseconds()); // p2 has 2 changesets ChangeSet process2Changes1 = TestChangeSet.create("j2", 1); journal.notify(process2Changes1); ChangeSet process2Changes2 = TestChangeSet.create("j2", 1); journal.notify(process2Changes2); timestamp2 = new ModeShapeDateTime(process2Changes2.getTimestamp().getMilliseconds()); // p3 has 2 changesets and 1 empty one ChangeSet process3Changes1 = TestChangeSet.create("j3", 2); journal.notify(process3Changes1); ChangeSet process3Changes2 = TestChangeSet.create("j3", 2); journal.notify(process3Changes2); timestamp3 = new ModeShapeDateTime(process3Changes2.getTimestamp().getMilliseconds()); ChangeSet process3Changes3 = TestChangeSet.create("j3", 0); journal.notify(process3Changes3); } @Test public void shouldDetectedStartedState() throws Exception { assertTrue("Journal should've been started", journal.started()); } @Test public void shouldReturnAllRecords() throws Exception { assertEquals(8, journal.allRecords(false).size()); } @Test public void shouldAddRecords() throws InterruptedException { int initialRecordCount = journal.allRecords(false).size(); journal.addRecords(new JournalRecord(TestChangeSet.create("j4", 2)), new JournalRecord(TestChangeSet.create("j4", 1)), new JournalRecord(TestChangeSet.create("j4", 3))); assertEquals(initialRecordCount + 3, journal.allRecords(false).size()); } @Test public void shouldReturnLastRecord() throws Exception { JournalRecord lastRecord = journal.lastRecord(); assertNotNull(lastRecord); assertEquals("j3", lastRecord.getJournalId()); assertEquals(timestamp3.getMilliseconds(), lastRecord.getChangeTimeMillis()); } @Test public void shouldSearchRecordsBasedOnTimestamp() throws Exception { // find records older than 1 day assertEquals(8, journal.recordsNewerThan(timestamp1.toLocalDateTime().minusDays(1), true, false).size()); // find records older than ts1, inclusive assertEquals(5, journal.recordsNewerThan(timestamp1.toLocalDateTime(), true, false).size()); // find records older than ts1, exclusive assertEquals(4, journal.recordsNewerThan(timestamp1.toLocalDateTime(), false, false).size()); // find records older than ts2, exclusive assertEquals(2, journal.recordsNewerThan(timestamp2.toLocalDateTime(), false, false).size()); // find records older than ts3, exclusive assertEquals(0, journal.recordsNewerThan(timestamp3.toLocalDateTime(), false, false).size()); // find records older than ts3, inclusive assertEquals(1, journal.recordsNewerThan(timestamp3.toLocalDateTime(), true, false).size()); // find records older than max, exclusive assertEquals(0, journal.recordsNewerThan(timestamp3.toLocalDateTime().plusDays(1), true, false).size()); } @Test @FixFor( "MODE-1903" ) public void shouldReturnNodeChangesBasedOnTimestamp() throws Exception { assertEquals(14, countChangedNodesSince(-1)); assertEquals(7, countChangedNodesSince(timestamp1.getMilliseconds())); assertEquals(5, countChangedNodesSince(timestamp2.getMilliseconds())); assertEquals(2, countChangedNodesSince(timestamp3.getMilliseconds())); assertEquals(0, countChangedNodesSince(Long.MAX_VALUE)); } private int countChangedNodesSince(long timestamp) { int count = 0; Iterator<NodeKey> iterator = journal.changedNodesSince(timestamp); while (iterator.hasNext()) { iterator.next(); ++count; } return count; } @Test public void shouldRemoveOlderLocalJournalEntries() throws Exception { LocalJournal localJournal = localJournal(); File journalFolder = new File(localJournal.getJournalLocation()); assertTrue(journalFolder.isDirectory() && journalFolder.canRead()); int initialEntriesCount = journal.allRecords(false).size(); // insert some of entries - this should create multiple files int entriesCount = 10; for (int i = 0; i < entriesCount; i++) { journal.notify(TestChangeSet.create("j1", entriesCount)); } // make sure we have at least 3 segments assertEquals(entriesCount + initialEntriesCount, journal.allRecords(false).size()); Thread.sleep(1); long currentMillis = System.currentTimeMillis(); Thread.sleep(1); // insert another batch of record which should produce additional segments for (int i = 0; i < entriesCount; i++) { journal.notify(TestChangeSet.create("j1", entriesCount)); } int sizeAfterSecondBatch = journal.allRecords(false).size(); assertEquals(entriesCount * 2 + initialEntriesCount, sizeAfterSecondBatch); // this should remove all the entries from the first batch + initial entries localJournal.removeRecordsOlderThan(currentMillis); assertEquals(entriesCount, journal.allRecords(false).size()); // now make sure we can still add data to the journal journal.notify(TestChangeSet.create("j4", 2)); assertEquals(entriesCount + 1, journal.allRecords(false).size()); } @Test public void shouldHaveSameJournalIdAfterRestart() throws Exception { String journalId = journal.journalId(); journal.shutdown(); journal.start(); assertEquals(journalId, journal.journalId()); } protected ChangeJournal journal() throws Exception { return new LocalJournal("target/local_journal"); } protected LocalJournal localJournal() { return (LocalJournal) journal; } static class TestChangeSet implements ChangeSet { private static final long serialVersionUID = 1L; private final String uuid = UUID.randomUUID().toString(); private List<Change> changes; private org.modeshape.jcr.api.value.DateTime timestamp; private String journalId; private Set<NodeKey> nodeChanges; private TestChangeSet( List<Change> changes, String journalId, Set<NodeKey> changedNodes) { this.changes = changes; this.timestamp = new ModeShapeDateTime(); this.journalId = journalId; this.nodeChanges = changedNodes; } @Override public int size() { return changes.size(); } @Override public boolean isEmpty() { return changes.isEmpty(); } @Override public String getUserId() { return "someUser"; } @Override public Map<String, String> getUserData() { return null; } @Override public DateTime getTimestamp() { return timestamp; } @Override public String getProcessKey() { return null; } @Override public String getRepositoryKey() { return "someRepository"; } @Override public String getWorkspaceName() { return "someWorkspace"; } @Override public Set<NodeKey> changedNodes() { return nodeChanges; } @Override public Set<BinaryKey> unusedBinaries() { return null; } @Override public Set<BinaryKey> usedBinaries() { return null; } @Override public boolean hasBinaryChanges() { return false; } @Override public String getSessionId() { return null; } @Override public String getJournalId() { return journalId; } @Override public Iterator<Change> iterator() { return changes.iterator(); } @Override public String getUUID() { return uuid; } @SuppressWarnings( "serial" ) static ChangeSet create( String journalId, int changesCount ) throws InterruptedException { List<Change> changes = new ArrayList<Change>(changesCount); for (int i = 0; i < changesCount; i++) { changes.add(new Change() {}); } Set<NodeKey> nodeChanges = new HashSet<>(changesCount); for (int i = 0; i < changesCount; i++) { nodeChanges.add(new NodeKey(UUID.randomUUID().toString())); } // sleep 1 second to make sure that successive calls won't have the same TS Thread.sleep(1); return new TestChangeSet(changes, journalId, nodeChanges); } } }