diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventCommitter.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventCommitter.java index 9534e395..43445698 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventCommitter.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventCommitter.java @@ -29,11 +29,9 @@ *

* Where the line falls is the subject of O12 in the event-encoding research, and it falls between the events * an application asked for by name and the ones it did not. At {@code IMMEDIATE}, {@code track} and - * {@code identify} write themselves before returning, because an application reporting an error is saying - * this matters more than the microseconds it costs, and the crash it describes may be moments away. They - * encode and write only themselves: what is held, and the summaries, which can take megabytes to encode, are - * left to a commit queued on the commit executor in the same call, so the exposures leading up to the error - * follow it moments later rather than costing the caller's thread. Everything else is committed once + * {@code identify} are commit points, because an application reporting an error is saying this matters more + * than the microseconds it costs, and the crash it describes may be moments away. A commit encodes the whole + * held run, so the exposures leading up to the error go down with it. Everything else is committed once * {@link #PENDING_COMMIT_THRESHOLD} events have accumulated where persistence is on, on {@code flush}, at a * delivery, on backgrounding, or when the application flushes from its own crash handler. Backgrounding * commits through {@code flush}: {@link ConnectivityManager} calls it on every move to the background, and @@ -46,10 +44,8 @@ * {@code DEFERRED} it costs what an evaluation does. *

* Threads. Recording runs on whichever thread evaluated or tracked. Commits run on {@link #commitExecutor}, - * or on the caller's thread for a flush, one at a time under {@link #commitLock}. A delivery commits too, on - * the event processor's delivery thread, before it closes a batch. A {@code track} or {@code identify} at - * {@code IMMEDIATE} writes its one event without {@link #commitLock}, so that it never waits for a commit - * encoding a run. + * or on the caller's thread at a commit point, one at a time under {@link #commitLock}. A delivery commits + * too, on the event processor's delivery thread, before it closes a batch. */ final class EventCommitter { /** @@ -63,15 +59,15 @@ final class EventCommitter { * could take. An uncaught exception runs the application's handler while the process is still alive, and * a handler that flushes commits the whole held run. So this is the window for the terminations that run * nothing on the way out -- {@code SIGKILL}, a native crash, an ANR kill, and the system reclaiming a - * backgrounded process. A {@code track} at {@code IMMEDIATE} narrows it too, because it queues a commit. + * backgrounded process. A {@code track} at {@code IMMEDIATE} closes it too, because it commits. *

- * The second is what a {@code flush} usually costs, since it encodes whatever is held before returning. + * The second is what such a {@code track} usually costs, since it encodes whatever is held before returning. * The commit executor keeps the run drained, so the common case is far below this; raising it trades that * cost against the number of writes. *

* Neither is a ceiling. Reaching this only schedules a commit, and recording does not wait for it, so * where events arrive faster than the commit executor encodes them, or the executor is starved, the run - * grows up to the event capacity, and the next flush encodes all of it. + * grows up to the event capacity, and the next commit point encodes all of it. */ static final int PENDING_COMMIT_THRESHOLD = 32; @@ -90,8 +86,7 @@ final class EventCommitter { */ private final int capacity; /** - * Whether {@code track} and {@code identify} write before returning, and a flush writes even where - * nothing persists, rather than leaving that work to a commit off the caller's thread. + * Whether a commit point encodes and writes before returning, rather than queuing that work. */ private final boolean commitOnCallerThread; /** The event processor's: set once it closes, after which nothing more is recorded. */ @@ -107,10 +102,10 @@ final class EventCommitter { /** * Held for the whole of a commit, so that only one runs at a time. *

- * This is what a flush's guarantee rests on. Without it a commit already in flight could take the - * caller's events out of {@link #pending} before the caller got there, leaving the caller nothing to + * This is what {@code track}'s guarantee rests on. Without it a commit already in flight could take the + * caller's event out of {@link #pending} before the caller got there, leaving the caller nothing to * write and returning while those bytes were still being produced somewhere else. Waiting here instead - * means that when the call returns the events are on disk, whichever commit put them there. + * means that when the call returns the event is on disk, whichever commit put it there. *

* Distinct from {@link #recordLock}, which is only held long enough to hand the run and the counters * over: encoding under this lock must not block a thread that is merely recording. @@ -121,7 +116,7 @@ final class EventCommitter { * The last recording a finished commit covered. Written under {@link #commitLock}, and read outside it * so that a caller whose recording is already durable does not queue for the lock at all. *

- * This is what makes concurrent flushes a group commit. The first caller's + * This is what makes concurrent {@code IMMEDIATE} commit points a group commit. The first caller's * commit takes every recording made before it took the run, including those of callers now waiting * behind it. They find their recordings covered and return, and the next caller still uncovered * commits everything that arrived in the meantime. Without it each waiter committed again in turn, @@ -156,18 +151,6 @@ final class EventCommitter { */ private final List pending = new ArrayList<>(); - /** - * Counters a {@code track} or {@code identify} at {@code IMMEDIATE} took from the summarizer for the next - * commit to encode, guarded by {@link #recordLock}. - *

- * Taken there rather than left, so the summarizer's contexts are cleared at each of them as they were when - * they committed the counters themselves: an application identifying its way through more contexts than - * capacity faster than the commit executor keeps up would otherwise find the cardinality limit binding. - * Every commit takes them along with {@link #pending}, so an evaluation's counter and its full event are - * still staged together. - */ - private final List takenSummaries = new ArrayList<>(); - /** * Numbers every recording, guarded by {@link #recordLock}, so a caller at a commit point can tell whether * a commit has already covered it. @@ -270,7 +253,8 @@ void recordEvaluation( } // Deliberately no commit point. An evaluation is expected to cost what a map lookup costs, and it // is usually the main thread doing it. The store writes these on its own thread once enough of them - // have piled up, and at IMMEDIATE the next track or identify queues that write as well. + // have piled up, and at IMMEDIATE the next event recorded at a commit point makes them durable along + // with itself. if (needsCommit) { scheduleCommitWherePersisting(); } @@ -283,7 +267,7 @@ void recordEvaluation( } /** - * Records an {@code identify} or {@code track} event, and at {@code IMMEDIATE} writes it before returning. + * Records an {@code identify} or {@code track} event, and at {@code IMMEDIATE} commits before returning. *

* At {@code DEFERRED} it writes nothing itself (durability ยง13.3), and is committed with what is held * at the pending threshold, a flush, a delivery, backgrounding or close. A commit per call would cost @@ -291,59 +275,14 @@ void recordEvaluation( * {@link #recordLock} from the threads evaluating flags and the store's locks from delivery. */ void recordAtCommitPoint(Event event) { - if (commitOnCallerThread && store.isPersisting()) { - writeAlone(event); - } else { - record(event); - } - } - - /** - * Encodes and writes one event on the caller's thread, and queues a commit of everything else. - *

- * Only the event itself, so the caller pays for one small encode and one write however much is held: - * a commit encodes the held run and every summary, and a summary with a large context or many flags - * takes megabytes, which is a frozen frame on the main thread. They follow on the commit executor, - * queued here. The event is written ahead of the run held before it, which LaunchDarkly does not mind: - * every event carries its own creation date. - *

- * Not under {@link #commitLock}, which a commit holds for the whole of its encoding, so this never waits - * for one. The store writes the one frame rather than committing, which would also write whatever a - * commit in flight has staged so far, and the encoder shares nothing between threads; the one wait left - * is for the store's write lock, held by a write or by a delivery reading a batch back. - */ - private void writeAlone(Event event) { - try { - boolean admitted; - synchronized (recordLock) { - // As in record: orders this against close()'s final commit, which this does not join, so an - // event admitted here is written before the store is released, and one refused never is. - if (closed.get()) { - return; - } - admitted = admit(event); - if (admitted) { - // Reserved where it is admitted, as a run is where it leaves pending, so capacity counts - // it while it is encoded and a recording on another thread cannot take its place. - store.reserve(1); - } - takenSummaries.addAll(eventBuffer.takeSummaries()); - summaryContextsExceeded.set(false); - } - if (admitted) { - byte[] serialized = eventBuffer.serialize(event); - if (serialized == null) { - // Counted by the commit queued below, which takes the failure. - store.releaseReservations(1); - } else if (!store.writeReservedAlone(serialized)) { - // Only the bytes are checked: a frame or byte capacity it turns out not to fit. - reportRefusedByStore("event", serialized); - } + long sequence = record(event); + if (commitOnCallerThread) { + try { + commitDurablyThrough(sequence); + } catch (RuntimeException e) { + // As in recordEvaluation: on the caller's thread, so a failure is logged, not thrown. + logUnexpectedError(e); } - scheduleCommit(); - } catch (RuntimeException e) { - // As in recordEvaluation: on the caller's thread, so a failure is logged, not thrown. - logUnexpectedError(e); } } @@ -534,12 +473,6 @@ void commitDurably() { run = pending.isEmpty() ? Collections.emptyList() : new ArrayList<>(pending); pending.clear(); summaries = eventBuffer.takeSummaries(); - if (!takenSummaries.isEmpty()) { - List taken = new ArrayList<>(takenSummaries); - taken.addAll(summaries); - takenSummaries.clear(); - summaries = taken; - } summaryContextsExceeded.set(false); // Reserved in the same critical section the run leaves pending in, so capacity counts // it in one place or the other throughout the encoding. Each staged event uses up its @@ -547,7 +480,11 @@ void commitDurably() { store.reserve(run.size()); } try { - stageRun(run); + try { + stageRun(run); + } finally { + store.releaseReservations(); + } stageSummaries(summaries); } catch (Throwable t) { // The run and the counters have left pending, and nothing puts them back, so a flush must not @@ -681,17 +618,10 @@ void shutdown() { * they finished encoding, which is not the order they were recorded in. */ private void stageRun(List run) { - int unused = run.size(); - try { - for (byte[] serialized : eventBuffer.serializeAll(run)) { - unused--; - if (!store.stageReserved(serialized)) { - reportRefusedByStore("event", serialized); - } + for (byte[] serialized : eventBuffer.serializeAll(run)) { + if (!store.stageReserved(serialized)) { + reportRefusedByStore("event", serialized); } - } finally { - // Only the run's own: a track or identify writing itself alone may hold one beside them. - store.releaseReservations(unused); } } @@ -730,21 +660,11 @@ private void reportRefusedByStore(String what, byte[] serialized) { * {@link #recordLock}. */ private void addPending(Event event) { - if (admit(event)) { - pending.add(event); - } - } - - /** - * Whether to keep an event, counting it as dropped if there is no room for it. Requires - * {@link #recordLock}. - */ - private boolean admit(Event event) { // Ahead of the capacity check, because an event the SDK was never going to send is not a // loss and must not be counted as one. Sampling and capacity are different reasons not to // keep an event, and only the second is one the SDK owes anyone a count of. if (!Sampler.shouldSample(event.getSamplingRatio())) { - return false; + return; } // The bytes are checked here too, before the event is encoded, so that one there is no room for // costs nothing; whether it fits is decided when it is staged. @@ -753,13 +673,13 @@ private boolean admit(Event event) { logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); } droppedEvents.incrementAndGet(); - return false; + return; } // Read before written: a write on every event would contend for the line with nothing to change. if (capacityExceeded.get()) { capacityExceeded.set(false); } - return true; + pending.add(event); } /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventStore.java index e3c6555c..65ada3fc 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventStore.java @@ -328,9 +328,8 @@ boolean admitsEvent(int heldElsewhere, int capacity) { * Counts events against capacity before they are staged, for a caller that has taken them from * somewhere capacity was already counting them and will encode them before staging. *

- * Each {@link #stageReserved} or {@link #writeReservedAlone} uses one up, and - * {@link #releaseReservations} gives back any that never made it, such as an event that could not be - * serialized. + * Each {@link #stageReserved} uses one up, and {@link #releaseReservations} gives back any that + * never made it, such as an event that could not be serialized. */ void reserve(int events) { synchronized (bufferLock) { @@ -338,14 +337,10 @@ void reserve(int events) { } } - /** - * Gives back {@code events} reservations not used up by {@link #stageReserved} or - * {@link #writeReservedAlone}. Only the caller's own, since another caller may hold reservations - * alongside. - */ - void releaseReservations(int events) { + /** Gives back every reservation not used up by {@link #stageReserved}. */ + void releaseReservations() { synchronized (bufferLock) { - reservedEvents = Math.max(0, reservedEvents - events); + reservedEvents = 0; } } @@ -390,9 +385,6 @@ boolean stageReserved(byte[] serializedEvent) { private boolean stage(byte[] serializedEvent, boolean bypassingCapacity, boolean reserved) { if (serializedEvent == null || serializedEvent.length == 0 || serializedEvent.length > EventLogFormat.MAX_FRAME_SIZE) { - if (reserved) { - releaseReservations(1); - } return false; } long frameSize = (long) serializedEvent.length + EventLogFormat.FRAME_OVERHEAD; @@ -525,56 +517,6 @@ void commit() { } } - /** - * Writes one serialized event to the log on its own, using up one reservation as - * {@link #stageReserved} does, but leaving what anything else has staged to the commit that staged it, - * so the caller pays for writing its own bytes and no one else's. It may land ahead of events staged - * before it. - *

- * Without persistence, or once it has failed, the event is staged in memory instead, as a commit - * leaves it. - * - * @return false if the event is too large for a frame or for the bytes left in the store - */ - boolean writeReservedAlone(byte[] serializedEvent) { - // As in commit: not worth waiting on ioLock for where there is nothing to write. - if (!persistEvents) { - return stageReserved(serializedEvent); - } - if (serializedEvent == null || serializedEvent.length == 0 - || serializedEvent.length > EventLogFormat.MAX_FRAME_SIZE) { - releaseReservations(1); - return false; - } - ByteArrayOutputStream framed = - new ByteArrayOutputStream(serializedEvent.length + EventLogFormat.FRAME_OVERHEAD); - EventLogFormat.writeFrame(framed, serializedEvent); - byte[] frame = framed.toByteArray(); - synchronized (ioLock) { - Staged alone; - synchronized (bufferLock) { - // In the same critical section as the count that replaces it, so the event is never - // counted twice or not at all. - if (reservedEvents > 0) { - reservedEvents--; - } - if (countedBytesHoldingBufferLock() + frame.length > eventByteCapacity) { - return false; - } - if (!persistEvents) { - writeQuietly(bufferData, frame); - bufferedEventCount++; - return true; - } - alone = new Staged(frame, frame.length, 1, 0); - inTransitEvents++; - inTransitBytes += frame.length; - } - writeHoldingIoLock(alone); - } - return true; - } - // ---- delivery ---- /** @@ -1133,11 +1075,6 @@ private void commitHoldingIoLock() { } staged = takeStagedHoldingBufferLock(); } - writeHoldingIoLock(staged); - } - - /** Requires {@code ioLock}, and {@code staged} counted in transit. */ - private void writeHoldingIoLock(Staged staged) { byte[] bytes = staged.frames; int written = 0; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventPersistence.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventPersistence.java index 785c7a40..387a53a5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventPersistence.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventPersistence.java @@ -43,11 +43,13 @@ public enum EventPersistence { /** * Events are written to disk before {@code track} and {@code identify} return. *

- * The write happens on the calling thread and covers that one event, costing tens to hundreds of - * microseconds depending on the device, however many evaluations are waiting. Those, and the summaries - * of them, are written on a thread of the SDK's that the call sets going, so they follow moments later. - * In exchange there is no window: a process that dies the instant after {@code track} returns still - * reports that event on the next run. Evaluating a flag stays in memory regardless. + * The write happens on the calling thread, costing tens to hundreds of microseconds depending on the + * device and on how many evaluations are waiting to be encoded alongside it. Evaluations are usually + * written in the background before many build up. A burst of them faster than that can leave up to the + * event capacity waiting, and the {@code track} or {@code identify} that follows encodes them all, which + * can take milliseconds. In exchange there is no window: a process that dies the instant after + * {@code track} returns still reports that event on the next run. Evaluating a flag stays in memory + * regardless. *

* Because this is disk I/O on the calling thread, an application that calls {@code track} or * {@code identify} on the main thread with {@link android.os.StrictMode} detecting disk writes will see diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java index 0489e132..29dabbea 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DirectEventProcessorTest.java @@ -228,9 +228,9 @@ public void immediatePersistencePutsATrackOnDiskBeforeItReturns() throws Excepti } @Test - public void anImmediateTrackWritesOnlyItselfAndLeavesTheRestToTheCommitExecutor() throws Exception { - // A summary can take megabytes to encode, which on the main thread is a frozen frame, so a track writes - // its own event and queues the rest. Holding the commit executor shows which write is whose. + public void anImmediateTrackPutsTheEvaluationsBeforeItOnDiskBeforeItReturns() throws Exception { + // The exposures leading up to an error are what a kill right after the track would otherwise take. + // Holding the commit executor leaves the track's own write as the only one there can be. ScheduledExecutorService scheduler = EventUtil.makeEventsTaskExecutor(); ExecutorService diagnosticExecutor = EventUtil.makeDiagnosticsTaskExecutor(); diagnosticExecutors.add(diagnosticExecutor); @@ -248,12 +248,6 @@ public void anImmediateTrackWritesOnlyItselfAndLeavesTheRestToTheCommitExecutor( eventProcessor.recordCustomEvent(CONTEXT, "fatal-error", LDValue.of("boom"), null); assertEquals(1, kindsOnDisk("custom")); - assertEquals(0, kindsOnDisk("feature")); - assertEquals(0, kindsOnDisk("summary")); - - commitsResume.countDown(); - lastCommitExecutor.submit(() -> { }).get(5, TimeUnit.SECONDS); - assertEquals(1, kindsOnDisk("feature")); assertEquals(1, kindsOnDisk("summary")); } finally { @@ -262,50 +256,6 @@ public void anImmediateTrackWritesOnlyItselfAndLeavesTheRestToTheCommitExecutor( } } - @Test - public void anImmediateTrackDoesNotWaitForACommitInProgress() throws Exception { - // The commit executor's commit is stalled while it holds the commit lock, as one encoding a large - // summary would be. The track has nothing to wait for it about. - CountDownLatch commitStalled = new CountDownLatch(1); - CountDownLatch commitResumes = new CountDownLatch(1); - EventStore store = new EventStore(EventDirectory.defaultDirectory(eventsDirectory.getRoot(), MOBILE_KEY), - DEFAULT_CAPACITY, true, logging.logger) { - @Override - void commit() { - if (Thread.currentThread().getName().startsWith("LaunchDarkly-event-store")) { - commitStalled.countDown(); - awaitQuietly(commitResumes, 10, TimeUnit.SECONDS); - } - super.commit(); - } - }; - ScheduledExecutorService scheduler = EventUtil.makeEventsTaskExecutor(); - ExecutorService diagnosticExecutor = EventUtil.makeDiagnosticsTaskExecutor(); - diagnosticExecutors.add(diagnosticExecutor); - DirectEventProcessor eventProcessor = makeEventProcessor(new StubEventSender(), - UNUSED_EVENTS_URI, null, NO_PERIODIC_FLUSH_MILLIS, 60_000, CLOSE_BUDGET_MILLIS, - scheduler, diagnosticExecutor, store, DEFAULT_CAPACITY, true); - try { - for (int i = 0; i < EventCommitter.PENDING_COMMIT_THRESHOLD; i++) { - eventProcessor.recordEvaluationEvent(CONTEXT, FLAG_KEY, FLAG_VERSION, VARIATION, - FLAG_VALUE, EvaluationReason.off(), DEFAULT_VALUE, true, null); - } - assertTrue("the threshold's commit never started", commitStalled.await(5, TimeUnit.SECONDS)); - - long start = System.nanoTime(); - eventProcessor.recordCustomEvent(CONTEXT, "fatal-error", LDValue.of("boom"), null); - long tookMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); - - assertTrue("the track waited " + tookMillis + "ms for the commit in progress", tookMillis < 1_000); - // Without what the stalled commit had staged before it stalled, which is that commit's to write. - assertEquals(1, kindsOnDisk("custom")); - assertEquals(0, kindsOnDisk("feature")); - } finally { - commitResumes.countDown(); - eventProcessor.close(); - } - } - @Test public void aDeferredTrackWritesNothingItselfAndReachesTheDiskAtThePendingThreshold() throws Exception { EventStore store = EventStore.create(eventsDirectory.getRoot(), MOBILE_KEY, @@ -1472,20 +1422,28 @@ public File getNoBackupFilesDir() { } @Test - public void concurrentImmediateTracksAreEachDurableOnReturn() throws Exception { + public void concurrentImmediateTracksShareCommitsAndEachIsDurableOnReturn() throws Exception { final int trackers = 8; + Queue staged = new ConcurrentLinkedQueue<>(); Queue durable = new ConcurrentLinkedQueue<>(); + AtomicInteger commits = new AtomicInteger(); EventStore store = new EventStore(eventsDirectory.newFolder(), DEFAULT_CAPACITY, true, logging.logger) { @Override - boolean writeReservedAlone(byte[] serializedEvent) { + boolean stageReserved(byte[] serializedEvent) { + staged.add(new String(serializedEvent, StandardCharsets.UTF_8)); + return super.stageReserved(serializedEvent); + } + + @Override + void commit() { + commits.incrementAndGet(); // A slow disk, so that the other tracks arrive while this write is under way. sleepQuietly(50); - boolean written = super.writeReservedAlone(serializedEvent); - if (written) { - durable.add(new String(serializedEvent, StandardCharsets.UTF_8)); - } - return written; + // Everything staged so far was staged under the commit lock this commit holds. + List writing = new ArrayList<>(staged); + super.commit(); + durable.addAll(writing); } }; ScheduledExecutorService scheduler = EventUtil.makeEventsTaskExecutor(); @@ -1520,6 +1478,8 @@ boolean writeReservedAlone(byte[] serializedEvent) { } assertEquals("a track returned before its event was on disk", 0, notDurableOnReturn.get()); + assertTrue("each track committed on its own: " + commits.get() + " commits for " + + trackers + " tracks", commits.get() < trackers); } finally { eventProcessor.close(); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventStoreTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventStoreTest.java index d9f5a235..afcf7f3f 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventStoreTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/EventStoreTest.java @@ -127,41 +127,6 @@ public void aCommittedEventIsOnTheDiskWhereAnotherProcessWouldFindIt() throws IO assertEquals(Arrays.asList("committed"), keysOnDisk()); } - @Test - public void anEventWrittenAloneLeavesWhatIsStagedToItsCommit() throws IOException { - EventStore store = store(); - store.stage(event("staged")); - store.reserve(1); - - assertTrue(store.writeReservedAlone(event("alone"))); - - assertEquals(Arrays.asList("alone"), keysOnDisk()); - assertEquals(2, store.getPendingEventCount()); - store.commit(); - assertEquals(Arrays.asList("alone", "staged"), keysOnDisk()); - } - - @Test - public void anEventReservedToBeWrittenAloneKeepsItsPlace() throws IOException { - EventStore store = new EventStore(directory(), 1, true, logger); - store.reserve(1); - - // What another thread recording meanwhile finds. - assertFalse(store.stage(event("staged"))); - assertTrue(store.writeReservedAlone(event("alone"))); - assertEquals(1, store.getPendingEventCount()); - } - - @Test - public void releasingReservationsReleasesOnlyThoseItIsToldTo() { - EventStore store = store(); - store.reserve(1); - store.reserve(2); - store.releaseReservations(2); - - assertEquals(1, store.getPendingEventCount()); - } - @Test public void theStoreNeverWritesOnItsOwnHoweverMuchIsStaged() throws IOException { // When to write is the caller's decision. 64 KB is well past what a store that wrote on its own would @@ -538,18 +503,4 @@ public void aStoreThatGaveUpOnTheDiskDeliversItsEventsFromMemoryInTheOrderTheyWe assertEquals(Collections.emptyList(), store.pendingBatches()); assertEquals(0, store.getPendingEventCount()); } - - @Test - public void anEventWrittenAloneIsDeliveredFromMemoryWhenTheWriteFails() throws IOException { - EventStore store = unwritableStore(CAPACITY); - - store.reserve(1); - assertTrue(store.writeReservedAlone(event("alone"))); - - assertFalse(store.isPersisting()); - assertEquals(1, store.getPendingEventCount()); - EventStore.Batch batch = closeOne(store); - assertEquals(1, batch.eventCount); - assertEquals(Arrays.asList("alone"), keysIn(store.body(batch))); - } }