// // Copyright (c) 2016 Couchbase, 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.couchbase.lite.multithreads; import com.couchbase.lite.CouchbaseLiteException; import com.couchbase.lite.CouchbaseLiteRuntimeException; import com.couchbase.lite.Database; import com.couchbase.lite.Document; import com.couchbase.lite.Emitter; import com.couchbase.lite.LiteTestCaseWithDB; import com.couchbase.lite.LiveQuery; import com.couchbase.lite.Mapper; import com.couchbase.lite.Query; import com.couchbase.lite.QueryEnumerator; import com.couchbase.lite.QueryRow; import com.couchbase.lite.Reducer; import com.couchbase.lite.View; import com.couchbase.lite.internal.RevisionInternal; import com.couchbase.lite.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Created by hideki on 6/29/16. */ public class MultiThreadsTest extends LiteTestCaseWithDB { private static final String TAG = MultiThreadsTest.class.getName(); @Override protected void setUp() throws Exception { if (!multithreadsTestsEnabled()) { return; } super.setUp(); } public void testInsertAndQueryThreads() { if (!multithreadsTestsEnabled()) return; // create view final View view = createView(database); final AtomicInteger count = new AtomicInteger(); Thread insertThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 500; i++) { String docID = String.format(Locale.ENGLISH, "docID-%08d", count.incrementAndGet()); Document doc = database.getDocument(docID); Map<String, Object> props = new HashMap<String, Object>(); props.put("key", count.get()); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in Document.putProperty(). ", e); fail(e.getMessage()); } } } }); Thread queryThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { Query query = view.createQuery(); try { for (QueryRow row : query.run()) { Log.i(TAG, "row=%s", row.getDocumentId()); } } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in Query.run(). ", e); fail(e.getMessage()); } } } }); queryThread.start(); insertThread.start(); try { insertThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in insertThread. ", e); } try { queryThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in insertThread. ", e); } } public static View createView(Database db) { return createView(db, "aview"); } public static View createView(Database db, String name) { View view = db.getView(name); if (view != null) { view.setMapReduce(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { if (document.get("key") != null) emitter.emit(document.get("key"), null); } }, null, "1"); } return view; } /** * https://github.com/couchbase/couchbase-lite-java-core/issues/1437 */ public void testUpdateDocsAndReadRevHistory() throws Exception { if (!multithreadsTestsEnabled()) return; // Insert docs Thread insertThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { String docID = String.format(Locale.ENGLISH, "docID-%08d", i); Document doc = database.getDocument(docID); Map<String, Object> props = new HashMap<String, Object>(); props.put("key", i); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in Document.putProperty(). ", e); fail(e.getMessage()); } } } }); insertThread.start(); try { insertThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in insertThread. ", e); } // Thread1: read docs Thread readThread = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 20; j++) { for (int i = 0; i < 100; i++) { String docID = String.format(Locale.ENGLISH, "docID-%08d", i); Document doc = database.getDocument(docID); String revID = doc.getCurrentRevisionId(); //RevisionInternal rev = new RevisionInternal(doc.getId(), revID, false); RevisionInternal rev = new RevisionInternal(doc.getId(), doc.getCurrentRevisionId(), false); //RevisionInternal rev = new RevisionInternal(doc.getId(), null, false); List<RevisionInternal> revs = database.getRevisionHistory(rev); //Log.e(TAG, "readThread docID=[%s] revID=[%s]", docID, revID); assertNotNull(revs); } } } }); // Thread2: update docs Thread updateThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { String docID = String.format(Locale.ENGLISH, "docID-%08d", i); Log.e(TAG, "updateThread docID=%s", docID); for (int j = 0; j < 20; j++) { Document doc = database.getDocument(docID); Map<String, Object> props = new HashMap<String, Object>(); props.putAll(doc.getProperties()); props.put("index", j); try { doc.putProperties(props); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in Document.putProperty(). ThreadName=[%s]", e, Thread.currentThread().getName()); fail(e.getMessage()); } } } } }); // run read thread and update thread in parallel readThread.start(); updateThread.start(); // wait read thread finish try { readThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in readThread. ", e); } // wait update thread finish try { updateThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in updateThread. ", e); } } /** * ported from .NET - TestMultipleQueriesOnSameView() * https://github.com/couchbase/couchbase-lite-net/blob/master/src/Couchbase.Lite.Tests.Shared/ViewsTest.cs#L2136 */ public void testMultipleQueriesOnSameView() throws CouchbaseLiteException { if (!multithreadsTestsEnabled()) return; View view = database.getView("view1"); view.setMapReduce(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { emitter.emit(document.get("jim"), document.get("_id")); } }, new Reducer() { @Override public Object reduce(List<Object> keys, List<Object> values, boolean rereduce) { return keys.size(); } }, "1.0"); LiveQuery query1 = view.createQuery().toLiveQuery(); LiveQuery query2 = view.createQuery().toLiveQuery(); try { query1.start(); query2.start(); long timestamp = System.currentTimeMillis(); for (int i = 0; i < 50; i++) { String docID = String.format(Locale.ENGLISH, "doc%d-%d", i, timestamp); Document doc = database.getDocument(docID); Map<String, Object> props = new HashMap<String, Object>(); props.put("jim", "borden"); doc.putProperties(props); } sleep(5 * 1000); assertEquals(50, view.getTotalRows()); assertEquals(50, view.getLastSequenceIndexed()); query1.stop(); for (int i = 50; i < 60; i++) { String docID = String.format(Locale.ENGLISH, "doc%d-%d", i, timestamp); Document doc = database.getDocument(docID); Map<String, Object> props = new HashMap<String, Object>(); props.put("jim", "borden"); doc.putProperties(props); if (i == 55) { query1.start(); } } sleep(5 * 1000); assertEquals(60, view.getTotalRows()); assertEquals(60, view.getLastSequenceIndexed()); } finally { query1.stop(); query2.stop(); } } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Log.e(TAG, "Error in Thread.sleep() - %d ms", e, millis); } } /** * parallel view/query without prefix */ public void testParallelViewQueries() throws CouchbaseLiteException { _testParallelViewQueries(""); } /** * parallel view/query with prefix */ public void testParallelViewQueriesWithPrefix() throws CouchbaseLiteException { _testParallelViewQueries("prefix/"); } /** * ported from .NET - TestParallelViewQueries() * https://github.com/couchbase/couchbase-lite-net/blob/master/src/Couchbase.Lite.Tests.Shared/ViewsTest.cs#L399 */ private void _testParallelViewQueries(String prefix) throws CouchbaseLiteException { if (!multithreadsTestsEnabled()) return; int[] data = new int[]{42, 184, 256, Integer.MAX_VALUE, 412}; final String viewName = prefix + "vu"; View vu = database.getView(viewName); vu.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", document.get("sequence")); emitter.emit(key, null); } }, "1.0"); final String viewNameFake = prefix + "vuFake"; View vuFake = database.getView(viewNameFake); vuFake.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", "FAKE"); emitter.emit(key, null); } }, "1.0"); createDocuments(database, 500); int expectCount = 1; parallelQuery(data, expectCount, viewName, viewNameFake); createDocuments(database, 500); expectCount = 2; parallelQuery(data, expectCount, viewName, viewNameFake); vu.delete(); vu = database.getView(viewName); vu.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", document.get("sequence")); emitter.emit(key, null); } }, "1.0"); expectCount = 2; parallelQuery(data, expectCount, viewName, viewNameFake); vuFake.delete(); vuFake = database.getView(viewNameFake); vuFake.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", "FAKE"); emitter.emit(key, null); } }, "1.0"); expectCount = 2; parallelQuery(data, expectCount, viewName, viewNameFake); } private void parallelQuery(int[] numbers, final int expectCount, final String viewName1, final String viewName2) { Thread[] threads = new Thread[numbers.length]; for (int i = 0; i < numbers.length; i++) { final int num = numbers[i]; threads[i] = new Thread(new Runnable() { @Override public void run() { try { if (num == Integer.MAX_VALUE) queryAction2(expectCount, viewName2); else queryAction(num, expectCount, viewName1); } catch (CouchbaseLiteException e) { e.printStackTrace(); fail(e.getMessage()); } } }); } for (int i = 0; i < numbers.length; i++) { threads[i].start(); } for (int i = 0; i < numbers.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void queryAction(int x, int expectCount, String viewName) throws CouchbaseLiteException { Database db = manager.getDatabase(database.getName()); View gotVu = db.getView(viewName); Query query = gotVu.createQuery(); List<Object> keys = new ArrayList<Object>(); Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", x); keys.add(key); query.setKeys(keys); QueryEnumerator rows = query.run(); assertEquals(expectCount * 500, gotVu.getLastSequenceIndexed()); assertEquals(expectCount, rows.getCount()); } private void queryAction2(int expectCount, String viewName) throws CouchbaseLiteException { Database db = manager.getDatabase(database.getName()); View gotVu = db.getView(viewName); Query query = gotVu.createQuery(); List<Object> keys = new ArrayList<Object>(); Map<String, Object> key = new HashMap<String, Object>(); key.put("sequence", "FAKE"); keys.add(key); query.setKeys(keys); QueryEnumerator rows = query.run(); assertEquals(expectCount * 500, gotVu.getLastSequenceIndexed()); assertEquals(expectCount * 500, rows.getCount()); } public void testCloseDBDuringInsertions() throws CouchbaseLiteException { if (!multithreadsTestsEnabled()) return; final Database db = manager.getDatabase("multi-thread-close-db"); final CountDownLatch latch = new CountDownLatch(50); Thread insertThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { store(i); latch.countDown(); } } }); Thread closeThread = new Thread(new Runnable() { @Override public void run() { try { assertTrue(latch.await(30, TimeUnit.SECONDS)); } catch (InterruptedException e) { fail(e.toString()); } db.close(); } }); closeThread.start(); insertThread.start(); try { closeThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in closeThread. ", e); } try { insertThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Error in insertThread. ", e); } db.delete(); } private void store(int i) { try { Database db = manager.getDatabase("multi-thread-close-db"); String id = String.format(Locale.ENGLISH, "ID_%05d", i); Document doc = db.getDocument(id); Map<String, Object> props = new HashMap<String, Object>(); props.put("value", i); doc.putProperties(props); } catch (CouchbaseLiteRuntimeException re) { Log.e(TAG, "Error in store(): %s", re.getMessage()); } catch (CouchbaseLiteException e) { Log.e(TAG, "Error in store()", e); fail("Error in store()"); } } }