/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.apache.activemq.artemis.tests.performance.storage;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.core.paging.PagingManager;
import org.apache.activemq.artemis.core.paging.PagingStore;
import org.apache.activemq.artemis.core.paging.cursor.PageCursorProvider;
import org.apache.activemq.artemis.core.paging.impl.Page;
import org.apache.activemq.artemis.core.persistence.OperationContext;
import org.apache.activemq.artemis.core.persistence.StorageManager;
import org.apache.activemq.artemis.core.replication.ReplicationManager;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.JournalType;
import org.apache.activemq.artemis.core.server.RouteContextList;
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.junit.Assert;
import org.junit.Test;
public class PersistMultiThreadTest extends ActiveMQTestBase {
final String DIRECTORY = "./target/journaltmp";
FakePagingStore fakePagingStore = new FakePagingStore();
@Test
public void testMultipleWrites() throws Exception {
deleteDirectory(new File(DIRECTORY));
ActiveMQServer server = createServer(true);
server.getConfiguration().setJournalCompactMinFiles(ActiveMQDefaultConfiguration.getDefaultJournalCompactMinFiles());
server.getConfiguration().setJournalCompactPercentage(ActiveMQDefaultConfiguration.getDefaultJournalCompactPercentage());
server.getConfiguration().setJournalDirectory(DIRECTORY + "/journal");
server.getConfiguration().setBindingsDirectory(DIRECTORY + "/bindings");
server.getConfiguration().setPagingDirectory(DIRECTORY + "/paging");
server.getConfiguration().setLargeMessagesDirectory(DIRECTORY + "/largemessage");
server.getConfiguration().setJournalFileSize(10 * 1024 * 1024);
server.getConfiguration().setJournalMinFiles(2);
server.getConfiguration().setJournalType(JournalType.ASYNCIO);
server.start();
StorageManager storage = server.getStorageManager();
long msgID = storage.generateID();
System.out.println("msgID=" + msgID);
int NUMBER_OF_THREADS = 50;
int NUMBER_OF_MESSAGES = 5000;
MyThread[] threads = new MyThread[NUMBER_OF_THREADS];
final CountDownLatch alignFlag = new CountDownLatch(NUMBER_OF_THREADS);
final CountDownLatch startFlag = new CountDownLatch(1);
final CountDownLatch finishFlag = new CountDownLatch(NUMBER_OF_THREADS);
MyDeleteThread deleteThread = new MyDeleteThread("deleteThread", storage, NUMBER_OF_MESSAGES * NUMBER_OF_THREADS * 10);
deleteThread.start();
for (int i = 0; i < threads.length; i++) {
threads[i] = new MyThread("writer::" + i, storage, NUMBER_OF_MESSAGES, alignFlag, startFlag, finishFlag);
}
for (MyThread t : threads) {
t.start();
}
alignFlag.await();
long startTime = System.currentTimeMillis();
startFlag.countDown();
// I'm using a countDown to avoid measuring time spent on thread context from join.
// i.e. i want to measure as soon as the loops are done
finishFlag.await();
long endtime = System.currentTimeMillis();
System.out.println("Time:: " + (endtime - startTime));
for (MyThread t : threads) {
t.join();
Assert.assertEquals(0, t.errors.get());
}
deleteThread.join();
Assert.assertEquals(0, deleteThread.errors.get());
}
LinkedBlockingDeque<Long> deletes = new LinkedBlockingDeque<>();
class MyThread extends Thread {
final StorageManager storage;
final int numberOfMessages;
final AtomicInteger errors = new AtomicInteger(0);
final CountDownLatch align;
final CountDownLatch start;
final CountDownLatch finish;
MyThread(String name,
StorageManager storage,
int numberOfMessages,
CountDownLatch align,
CountDownLatch start,
CountDownLatch finish) {
super(name);
this.storage = storage;
this.numberOfMessages = numberOfMessages;
this.align = align;
this.start = start;
this.finish = finish;
}
@Override
public void run() {
try {
align.countDown();
start.await();
long id = storage.generateID();
long txID = storage.generateID();
// each thread will store a single message that will never be deleted, trying to force compacting to happen
storeMessage(txID, id);
storage.commit(txID);
OperationContext ctx = storage.getContext();
for (int i = 0; i < numberOfMessages; i++) {
txID = storage.generateID();
long[] messageID = new long[10];
for (int msgI = 0; msgI < 10; msgI++) {
id = storage.generateID();
messageID[msgI] = id;
storeMessage(txID, id);
}
storage.commit(txID);
ctx.waitCompletion();
for (long deleteID : messageID) {
deletes.add(deleteID);
}
}
} catch (Exception e) {
e.printStackTrace();
errors.incrementAndGet();
} finally {
finish.countDown();
}
}
private void storeMessage(long txID, long id) throws Exception {
Message message = new CoreMessage(id, 10 * 1024);
message.setContext(fakePagingStore);
message.getBodyBuffer().writeBytes(new byte[104]);
message.putStringProperty("hello", "" + id);
storage.storeMessageTransactional(txID, message);
storage.storeReferenceTransactional(txID, 1, id);
message.decrementRefCount();
}
}
class MyDeleteThread extends Thread {
final StorageManager storage;
final int numberOfMessages;
final AtomicInteger errors = new AtomicInteger(0);
MyDeleteThread(String name, StorageManager storage, int numberOfMessages) {
super(name);
this.storage = storage;
this.numberOfMessages = numberOfMessages;
}
@Override
public void run() {
long deletesNr = 0;
try {
for (int i = 0; i < numberOfMessages; i++) {
if (i % 1000 == 0) {
// storage.getContext().waitCompletion();
// deletesNr = 0;
// Thread.sleep(200);
}
deletesNr++;
Long deleteID = deletes.poll(10, TimeUnit.MINUTES);
if (deleteID == null) {
System.err.println("Coudn't poll delete info");
errors.incrementAndGet();
break;
}
storage.storeAcknowledge(1, deleteID);
storage.deleteMessage(deleteID);
}
} catch (Exception e) {
e.printStackTrace(System.out);
errors.incrementAndGet();
} finally {
System.err.println("Finished the delete loop!!!! deleted " + deletesNr);
}
}
}
class FakePagingStore implements PagingStore {
@Override
public void durableDown(Message message, int durableCount) {
}
@Override
public void durableUp(Message message, int durableCount) {
}
@Override
public void nonDurableUp(Message message, int nonDurableCoun) {
}
@Override
public void nonDurableDown(Message message, int nonDurableCoun) {
}
@Override
public SimpleString getAddress() {
return null;
}
@Override
public int getNumberOfPages() {
return 0;
}
@Override
public int getCurrentWritingPage() {
return 0;
}
@Override
public SimpleString getStoreName() {
return null;
}
@Override
public File getFolder() {
return null;
}
@Override
public AddressFullMessagePolicy getAddressFullMessagePolicy() {
return null;
}
@Override
public long getFirstPage() {
return 0;
}
@Override
public long getPageSizeBytes() {
return 0;
}
@Override
public long getAddressSize() {
return 0;
}
@Override
public long getMaxSize() {
return 0;
}
@Override
public boolean isFull() {
return false;
}
@Override
public boolean isRejectingMessages() {
return false;
}
@Override
public void applySetting(AddressSettings addressSettings) {
}
@Override
public boolean isPaging() {
return false;
}
@Override
public void sync() throws Exception {
}
@Override
public void ioSync() throws Exception {
}
@Override
public boolean page(Message message,
Transaction tx,
RouteContextList listCtx,
ReentrantReadWriteLock.ReadLock readLock) throws Exception {
return false;
}
@Override
public Page createPage(int page) throws Exception {
return null;
}
@Override
public boolean checkPageFileExists(int page) throws Exception {
return false;
}
@Override
public PagingManager getPagingManager() {
return null;
}
@Override
public PageCursorProvider getCursorProvider() {
return null;
}
@Override
public void processReload() throws Exception {
}
@Override
public Page depage() throws Exception {
return null;
}
@Override
public void forceAnotherPage() throws Exception {
}
@Override
public Page getCurrentPage() {
return null;
}
@Override
public boolean startPaging() throws Exception {
return false;
}
@Override
public void stopPaging() throws Exception {
}
@Override
public void addSize(int size) {
}
@Override
public boolean checkMemory(Runnable runnable) {
return false;
}
@Override
public boolean lock(long timeout) {
return false;
}
@Override
public void unlock() {
}
@Override
public void flushExecutors() {
}
@Override
public Collection<Integer> getCurrentIds() throws Exception {
return null;
}
@Override
public void sendPages(ReplicationManager replicator, Collection<Integer> pageIds) throws Exception {
}
@Override
public void disableCleanup() {
}
@Override
public void enableCleanup() {
}
@Override
public void start() throws Exception {
}
@Override
public void stop() throws Exception {
}
@Override
public boolean isStarted() {
return false;
}
@Override
public boolean checkReleasedMemory() {
return true;
}
}
}