/* * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Thierry Delprat <tdelprat@nuxeo.com> * Antoine Taillefer <ataillefer@nuxeo.com> * */ package org.nuxeo.ecm.automation.server.jaxrs.batch; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import org.apache.commons.collections.ListUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.impl.blob.FileBlob; import org.nuxeo.ecm.core.test.CoreFeature; import org.nuxeo.ecm.core.transientstore.AbstractTransientStore; import org.nuxeo.ecm.core.transientstore.api.TransientStore; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.test.runner.Deploy; import org.nuxeo.runtime.test.runner.Features; import org.nuxeo.runtime.test.runner.FeaturesRunner; import org.nuxeo.runtime.test.runner.RuntimeHarness; import org.nuxeo.transientstore.test.TransientStoreFeature; /** * @since 7.10 */ @RunWith(FeaturesRunner.class) @Features({ CoreFeature.class, TransientStoreFeature.class }) @Deploy({ "org.nuxeo.ecm.automation.core", "org.nuxeo.ecm.platform.web.common", "org.nuxeo.ecm.webengine.core", "org.nuxeo.ecm.automation.io", "org.nuxeo.ecm.automation.server" }) public class BatchManagerFixture { @Inject protected RuntimeHarness harness; @Test public void testServiceRegistred() { BatchManager bm = Framework.getService(BatchManager.class); assertNotNull(bm); } @Test public void testTransientStoreRegistered() { BatchManager bm = Framework.getService(BatchManager.class); assertNotNull(bm.getTransientStore()); } @Test public void testBatchInit() throws Exception { BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); assertNotNull(batchId); assertTrue(bm.hasBatch(batchId)); Batch batch = ((BatchManagerComponent) bm).getBatch(batchId); assertNotNull(batch); assertEquals(batchId, batch.getKey()); // Check TransientStore storage size assertEquals(0, bm.getTransientStore().getStorageSizeMB()); } @Test(expected = NuxeoException.class) public void testBatchInitClientGeneratedIdNotAllowed() throws Exception { ((BatchManagerComponent) Framework.getService(BatchManager.class)).initBatchInternal("testBatchId"); } @Test public void testBatchInitClientGeneratedIdAllowed() throws Exception { harness.deployContrib("org.nuxeo.ecm.automation.test.test", "test-batchmanager-client-generated-id-allowed-contrib.xml"); BatchManager bm = Framework.getService(BatchManager.class); String batchId = ((BatchManagerComponent) bm).initBatchInternal("testBatchId").getKey(); assertEquals("testBatchId", batchId); assertTrue(bm.hasBatch("testBatchId")); Batch batch = ((BatchManagerComponent) bm).getBatch("testBatchId"); assertNotNull(batch); assertEquals("testBatchId", batch.getKey()); harness.undeployContrib("org.nuxeo.ecm.automation.test.test", "test-batchmanager-client-generated-id-allowed-contrib.xml"); } @Test public void testAddFileStream() throws IOException { // Add 2 file streams BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); InputStream is = new ByteArrayInputStream("Contenu accentué".getBytes("UTF-8")); bm.addStream(batchId, "0", is, "Mon doc 1.txt", "text/plain"); is = new ByteArrayInputStream("Autre contenu accentué".getBytes("UTF-8")); bm.addStream(batchId, "1", is, "Mon doc 2.txt", "text/plain"); // Check batch blobs Blob blob1 = bm.getBlob(batchId, "0"); assertEquals("Mon doc 1.txt", blob1.getFilename()); assertEquals("text/plain", blob1.getMimeType()); assertEquals("Contenu accentué", blob1.getString()); Blob blob2 = bm.getBlob(batchId, "1"); assertEquals("Mon doc 2.txt", blob2.getFilename()); assertEquals("text/plain", blob2.getMimeType()); assertEquals("Autre contenu accentué", blob2.getString()); List<Blob> blobs = bm.getBlobs(batchId); assertEquals(2, blobs.size()); assertEquals(blob1, blobs.get(0)); assertEquals(blob2, blobs.get(1)); // Check transient store // Batch entry Batch batch = ((BatchManagerComponent) bm).getBatch(batchId); assertNotNull(batch); assertEquals(batchId, batch.getKey()); assertEquals(blob1, batch.getBlob("0")); assertEquals(blob2, batch.getBlob("1")); assertTrue(ListUtils.isEqualList(blobs, batch.getBlobs())); // Batch file entries List<BatchFileEntry> batchFileEntries = batch.getFileEntries(); assertEquals(2, batchFileEntries.size()); BatchFileEntry fileEntry1 = batchFileEntries.get(0); assertEquals(batchId + "_0", fileEntry1.getKey()); assertFalse(fileEntry1.isChunked()); assertEquals("Mon doc 1.txt", fileEntry1.getFileName()); assertEquals("text/plain", fileEntry1.getMimeType()); assertEquals(17, fileEntry1.getFileSize()); assertEquals(blob1, fileEntry1.getBlob()); BatchFileEntry fileEntry2 = batchFileEntries.get(1); assertEquals(batchId + "_1", fileEntry2.getKey()); assertFalse(fileEntry2.isChunked()); assertEquals("Mon doc 2.txt", fileEntry2.getFileName()); assertEquals("text/plain", fileEntry2.getMimeType()); assertEquals(23, fileEntry2.getFileSize()); assertEquals(blob2, fileEntry2.getBlob()); // Check TransientStore storage size assertEquals(40, ((AbstractTransientStore) bm.getTransientStore()).getStorageSize()); } @Test public void testAddChunkStream() throws IOException { // Add 3 chunk streams in disorder BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); String fileContent = "Contenu accentué composé de 3 chunks"; String chunk1 = "Contenu accentu"; String chunk2 = "é composé de "; String chunk3 = "3 chunks"; long fileSize = fileContent.getBytes().length; bm.addStream(batchId, "0", new ByteArrayInputStream(chunk1.getBytes("UTF-8")), 3, 0, "Mon doc.txt", "text/plain", fileSize); bm.addStream(batchId, "0", new ByteArrayInputStream(chunk3.getBytes("UTF-8")), 3, 2, "Mon doc.txt", "text/plain", fileSize); bm.addStream(batchId, "0", new ByteArrayInputStream(chunk2.getBytes("UTF-8")), 3, 1, "Mon doc.txt", "text/plain", fileSize); // Check batch blobs Blob blob = bm.getBlob(batchId, "0"); bm.getBlob(batchId, "0"); assertEquals("Mon doc.txt", blob.getFilename()); assertEquals("text/plain", blob.getMimeType()); assertEquals(fileContent, blob.getString()); // Check transient store // Batch entry Batch batch = ((BatchManagerComponent) bm).getBatch(batchId); assertNotNull(batch); assertEquals(batchId, batch.getKey()); assertEquals(blob, batch.getBlob("0")); // Batch file entries List<BatchFileEntry> batchFileEntries = batch.getFileEntries(); assertEquals(1, batchFileEntries.size()); BatchFileEntry fileEntry = batchFileEntries.get(0); assertEquals(batchId + "_0", fileEntry.getKey()); assertTrue(fileEntry.isChunked()); assertEquals("Mon doc.txt", fileEntry.getFileName()); assertEquals("text/plain", fileEntry.getMimeType()); assertEquals(fileSize, fileEntry.getFileSize()); assertEquals(3, fileEntry.getChunkCount()); assertEquals(Arrays.asList(0, 1, 2), fileEntry.getOrderedChunkIndexes()); assertEquals(blob, fileEntry.getBlob()); // Batch chunk entries Collection<String> chunkEntryKeys = fileEntry.getChunkEntryKeys(); assertEquals(3, chunkEntryKeys.size()); String chunkEntryKey1 = batchId + "_0_0"; assertTrue(chunkEntryKeys.contains(chunkEntryKey1)); TransientStore ts = bm.getTransientStore(); List<Blob> chunkEntryBlobs = ts.getBlobs(chunkEntryKey1); assertEquals(1, chunkEntryBlobs.size()); Blob blob1 = chunkEntryBlobs.get(0); assertEquals(chunk1, blob1.getString()); assertEquals(15, blob1.getLength()); String chunkEntryKey2 = batchId + "_0_1"; assertTrue(chunkEntryKeys.contains(chunkEntryKey2)); chunkEntryBlobs = ts.getBlobs(chunkEntryKey2); assertEquals(1, chunkEntryBlobs.size()); Blob blob2 = chunkEntryBlobs.get(0); assertEquals(chunk2, blob2.getString()); assertEquals(15, blob2.getLength()); String chunkEntryKey3 = batchId + "_0_2"; assertTrue(chunkEntryKeys.contains(chunkEntryKey3)); chunkEntryBlobs = ts.getBlobs(chunkEntryKey3); assertEquals(1, chunkEntryBlobs.size()); Blob blob3 = chunkEntryBlobs.get(0); assertEquals(chunk3, blob3.getString()); assertEquals(8, blob3.getLength()); // Check TransientStore storage size assertEquals(38, ((AbstractTransientStore) ts).getStorageSize()); // Clean batch bm.clean(batchId); assertEquals(0, ts.getStorageSizeMB()); } @Test public void testBatchCleanup() throws IOException { BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); assertNotNull(batchId); // Add non chunked files for (int i = 0; i < 10; i++) { bm.addStream(batchId, "" + i, new ByteArrayInputStream(("SomeContent" + i).getBytes()), i + ".txt", "text/plain"); } // Add chunked file bm.addStream(batchId, "10", new ByteArrayInputStream(("Chunk 1 ").getBytes()), 2, 0, "chunkedFile.txt", "text/plain", 16); bm.addStream(batchId, "10", new ByteArrayInputStream(("Chunk 2 ").getBytes()), 2, 1, "chunkedFile.txt", "text/plain", 16); List<Blob> blobs = bm.getBlobs(batchId); assertNotNull(blobs); Assert.assertEquals(11, blobs.size()); Assert.assertEquals("4.txt", blobs.get(4).getFilename()); Assert.assertEquals("SomeContent7", blobs.get(7).getString()); Assert.assertEquals("Chunk 1 Chunk 2 ", blobs.get(10).getString()); // Batch data TransientStore ts = bm.getTransientStore(); assertTrue(ts.exists(batchId)); assertTrue(ts.exists(batchId + "_5")); assertTrue(ts.exists(batchId + "_10")); assertTrue(ts.exists(batchId + "_10_0")); assertTrue(ts.exists(batchId + "_10_1")); // Batch non chunked file FileBlob fileBlob = (FileBlob) blobs.get(9); File tmpFile = fileBlob.getFile(); assertNotNull(tmpFile); assertTrue(tmpFile.exists()); // Batch chunked file FileBlob chunkedFileBlob = (FileBlob) blobs.get(10); File tmpChunkedFile = chunkedFileBlob.getFile(); assertNotNull(tmpChunkedFile); assertTrue(tmpChunkedFile.exists()); bm.clean(batchId); // Batch data has been removed from cache as well as temporary chunked file, and non chunked file assertFalse(ts.exists(batchId)); assertFalse(ts.exists(batchId + "_5")); assertFalse(ts.exists(batchId + "_10")); assertFalse(ts.exists(batchId + "_10_0")); assertFalse(ts.exists(batchId + "_10_1")); assertFalse(tmpChunkedFile.exists()); assertFalse(tmpFile.exists()); assertEquals(0, ts.getStorageSizeMB()); } @Test public void testBatchConcurrency() throws Exception { BatchManager bm = Framework.getService(BatchManager.class); // Initialize batches with one file concurrently int nbBatches = 100; String[] batchIds = new String[nbBatches]; ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 5, 500L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(nbBatches + 1)); for (int i = 0; i < nbBatches; i++) { final int batchIndex = i; tpe.submit(new Runnable() { @Override public void run() { try { String batchId = bm.initBatch(); bm.addStream(batchId, "0", new ByteArrayInputStream(("SomeContent_" + batchId).getBytes(StandardCharsets.UTF_8)), "MyBatchFile.txt", "text/plain"); batchIds[batchIndex] = batchId; } catch (IOException e) { fail(e.getMessage()); } } }); } tpe.shutdown(); boolean finish = tpe.awaitTermination(20, TimeUnit.SECONDS); assertTrue("timeout", finish); // Check batches for (String batchId : batchIds) { assertNotNull(batchId); } // Test indexes 0, 9, 99, ..., nbFiles - 1 int nbDigits = (int) (Math.log10(nbBatches) + 1); int divisor = nbBatches; for (int i = 0; i < nbDigits; i++) { int batchIndex = nbBatches / divisor - 1; String batchId = batchIds[batchIndex]; Blob blob = bm.getBlob(batchId, "0"); assertNotNull(blob); assertEquals("MyBatchFile.txt", blob.getFilename()); assertEquals("SomeContent_" + batchId, blob.getString()); divisor = divisor / 10; } // Check storage size TransientStore ts = bm.getTransientStore(); assertTrue(((AbstractTransientStore) ts).getStorageSize() > 12 * nbBatches); // Clean batches for (String batchId : batchIds) { bm.clean(batchId); } assertEquals(ts.getStorageSizeMB(), 0); } @Test public void testFileConcurrency() throws Exception { // Initialize a batch BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); // Add files concurrently int nbFiles = 100; ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 5, 500L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(nbFiles + 1)); for (int i = 0; i < nbFiles; i++) { final String fileIndex = String.valueOf(i); tpe.submit(new Runnable() { @Override public void run() { try { bm.addStream( batchId, fileIndex, new ByteArrayInputStream(("SomeContent_" + fileIndex).getBytes(StandardCharsets.UTF_8)), fileIndex + ".txt", "text/plain"); } catch (IOException e) { fail(e.getMessage()); } } }); } tpe.shutdown(); boolean finish = tpe.awaitTermination(20, TimeUnit.SECONDS); assertTrue("timeout", finish); // Check blobs List<Blob> blobs = bm.getBlobs(batchId); assertEquals(nbFiles, blobs.size()); // Test indexes 0, 9, 99, ..., nbFiles - 1 int nbDigits = (int) (Math.log10(nbFiles) + 1); int divisor = nbFiles; for (int i = 0; i < nbDigits; i++) { int fileIndex = nbFiles / divisor - 1; assertEquals(fileIndex + ".txt", blobs.get(fileIndex).getFilename()); assertEquals("SomeContent_" + fileIndex, blobs.get(fileIndex).getString()); divisor = divisor / 10; } // Check storage size TransientStore ts = bm.getTransientStore(); assertTrue(((AbstractTransientStore) ts).getStorageSize() > 12 * nbFiles); // Clean batch bm.clean(batchId); assertEquals(ts.getStorageSizeMB(), 0); } @Test public void testChunkConcurrency() throws Exception { // Initialize a batch BatchManager bm = Framework.getService(BatchManager.class); String batchId = bm.initBatch(); // Add chunks concurrently int nbChunks = 100; ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 5, 500L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(nbChunks + 1)); for (int i = 0; i < nbChunks; i++) { final int chunkIndex = i; tpe.submit(new Runnable() { @Override public void run() { try { bm.addStream( batchId, "0", new ByteArrayInputStream( ("SomeChunkContent_" + chunkIndex + " ").getBytes(StandardCharsets.UTF_8)), nbChunks, chunkIndex, "MyChunkedFile.txt", "text/plain", 0); } catch (IOException e) { fail(e.getMessage()); } } }); } tpe.shutdown(); boolean finish = tpe.awaitTermination(20, TimeUnit.SECONDS); assertTrue("timeout", finish); // Check chunked file Blob blob = bm.getBlob(batchId, "0"); assertNotNull(blob); int nbOccurrences = 0; Pattern p = Pattern.compile("SomeChunkContent_"); Matcher m = p.matcher(blob.getString()); while (m.find()) { nbOccurrences++; } assertEquals(nbChunks, nbOccurrences); // Check storage size TransientStore ts = bm.getTransientStore(); assertTrue(((AbstractTransientStore) ts).getStorageSize() > 17 * nbChunks); // Clean batch bm.clean(batchId); assertEquals(ts.getStorageSizeMB(), 0); } }