HIGUCHI Yuta

Fix for ONOS-4315

- Additional log on error
- Allow count=0 using CountDownCompleter
- test case to detect the issue (@Ignored by default right now)
- other bug fixes found along the way

Based on patch by Madan@China

Change-Id: I7d6cb8c214052859900ef7ee0337a7e1c8a9d295
...@@ -18,6 +18,7 @@ package org.onosproject.store.primitives.resources.impl; ...@@ -18,6 +18,7 @@ package org.onosproject.store.primitives.resources.impl;
18 import static org.onosproject.store.service.MapEvent.Type.INSERT; 18 import static org.onosproject.store.service.MapEvent.Type.INSERT;
19 import static org.onosproject.store.service.MapEvent.Type.REMOVE; 19 import static org.onosproject.store.service.MapEvent.Type.REMOVE;
20 import static org.onosproject.store.service.MapEvent.Type.UPDATE; 20 import static org.onosproject.store.service.MapEvent.Type.UPDATE;
21 +import static org.slf4j.LoggerFactory.getLogger;
21 import io.atomix.copycat.server.session.ServerSession; 22 import io.atomix.copycat.server.session.ServerSession;
22 import io.atomix.copycat.server.Commit; 23 import io.atomix.copycat.server.Commit;
23 import io.atomix.copycat.server.Snapshottable; 24 import io.atomix.copycat.server.Snapshottable;
...@@ -60,7 +61,9 @@ import org.onosproject.store.primitives.resources.impl.AtomixConsistentMapComman ...@@ -60,7 +61,9 @@ import org.onosproject.store.primitives.resources.impl.AtomixConsistentMapComman
60 import org.onosproject.store.service.MapEvent; 61 import org.onosproject.store.service.MapEvent;
61 import org.onosproject.store.service.MapTransaction; 62 import org.onosproject.store.service.MapTransaction;
62 import org.onosproject.store.service.Versioned; 63 import org.onosproject.store.service.Versioned;
64 +import org.slf4j.Logger;
63 65
66 +import com.google.common.base.Throwables;
64 import com.google.common.collect.Lists; 67 import com.google.common.collect.Lists;
65 import com.google.common.collect.Maps; 68 import com.google.common.collect.Maps;
66 import com.google.common.collect.Sets; 69 import com.google.common.collect.Sets;
...@@ -72,6 +75,7 @@ import static com.google.common.base.Preconditions.checkState; ...@@ -72,6 +75,7 @@ import static com.google.common.base.Preconditions.checkState;
72 */ 75 */
73 public class AtomixConsistentMapState extends ResourceStateMachine implements SessionListener, Snapshottable { 76 public class AtomixConsistentMapState extends ResourceStateMachine implements SessionListener, Snapshottable {
74 77
78 + private final Logger log = getLogger(getClass());
75 private final Map<Long, Commit<? extends AtomixConsistentMapCommands.Listen>> listeners = new HashMap<>(); 79 private final Map<Long, Commit<? extends AtomixConsistentMapCommands.Listen>> listeners = new HashMap<>();
76 private final Map<String, MapEntryValue> mapEntries = new HashMap<>(); 80 private final Map<String, MapEntryValue> mapEntries = new HashMap<>();
77 private final Set<String> preparedKeys = Sets.newHashSet(); 81 private final Set<String> preparedKeys = Sets.newHashSet();
...@@ -384,7 +388,7 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se ...@@ -384,7 +388,7 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se
384 } 388 }
385 MapEntryValue existingValue = mapEntries.get(key); 389 MapEntryValue existingValue = mapEntries.get(key);
386 if (existingValue == null) { 390 if (existingValue == null) {
387 - if (update.currentValue() != null) { 391 + if (update.type() != MapUpdate.Type.PUT_IF_ABSENT) {
388 return PrepareResult.OPTIMISTIC_LOCK_FAILURE; 392 return PrepareResult.OPTIMISTIC_LOCK_FAILURE;
389 } 393 }
390 } else { 394 } else {
...@@ -399,6 +403,9 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se ...@@ -399,6 +403,9 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se
399 transaction.updates().forEach(u -> preparedKeys.add(u.key())); 403 transaction.updates().forEach(u -> preparedKeys.add(u.key()));
400 ok = true; 404 ok = true;
401 return PrepareResult.OK; 405 return PrepareResult.OK;
406 + } catch (Exception e) {
407 + log.warn("Failure applying {}", commit, e);
408 + throw Throwables.propagate(e);
402 } finally { 409 } finally {
403 if (!ok) { 410 if (!ok) {
404 commit.close(); 411 commit.close();
...@@ -416,6 +423,9 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se ...@@ -416,6 +423,9 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se
416 TransactionId transactionId = commit.operation().transactionId(); 423 TransactionId transactionId = commit.operation().transactionId();
417 try { 424 try {
418 return commitInternal(transactionId); 425 return commitInternal(transactionId);
426 + } catch (Exception e) {
427 + log.warn("Failure applying {}", commit, e);
428 + throw Throwables.propagate(e);
419 } finally { 429 } finally {
420 commit.close(); 430 commit.close();
421 } 431 }
...@@ -438,12 +448,11 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se ...@@ -438,12 +448,11 @@ public class AtomixConsistentMapState extends ResourceStateMachine implements Se
438 List<MapEvent<String, byte[]>> eventsToPublish = Lists.newArrayList(); 448 List<MapEvent<String, byte[]>> eventsToPublish = Lists.newArrayList();
439 for (MapUpdate<String, byte[]> update : transaction.updates()) { 449 for (MapUpdate<String, byte[]> update : transaction.updates()) {
440 String key = update.key(); 450 String key = update.key();
451 + checkState(preparedKeys.remove(key), "key is not prepared");
441 MapEntryValue previousValue = mapEntries.remove(key); 452 MapEntryValue previousValue = mapEntries.remove(key);
442 MapEntryValue newValue = null; 453 MapEntryValue newValue = null;
443 - checkState(preparedKeys.remove(key), "key is not prepared");
444 if (update.type() != MapUpdate.Type.REMOVE_IF_VERSION_MATCH) { 454 if (update.type() != MapUpdate.Type.REMOVE_IF_VERSION_MATCH) {
445 - newValue = new TransactionalCommit(key, 455 + newValue = new TransactionalCommit(key, versionCounter.incrementAndGet(), completer);
446 - versionCounter.incrementAndGet(), completer);
447 } 456 }
448 eventsToPublish.add(new MapEvent<>("", key, toVersioned(newValue), toVersioned(previousValue))); 457 eventsToPublish.add(new MapEvent<>("", key, toVersioned(newValue), toVersioned(previousValue)));
449 if (newValue != null) { 458 if (newValue != null) {
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
16 package org.onosproject.store.primitives.resources.impl; 16 package org.onosproject.store.primitives.resources.impl;
17 17
18 import io.atomix.resource.ResourceType; 18 import io.atomix.resource.ResourceType;
19 +
20 +import static org.hamcrest.Matchers.*;
19 import static org.junit.Assert.*; 21 import static org.junit.Assert.*;
20 22
21 import java.util.Arrays; 23 import java.util.Arrays;
...@@ -348,6 +350,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase { ...@@ -348,6 +350,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase {
348 350
349 map.addListener(listener).join(); 351 map.addListener(listener).join();
350 352
353 + // PUT_IF_ABSENT
351 MapUpdate<String, byte[]> update1 = 354 MapUpdate<String, byte[]> update1 =
352 MapUpdate.<String, byte[]>newBuilder().withType(MapUpdate.Type.PUT_IF_ABSENT) 355 MapUpdate.<String, byte[]>newBuilder().withType(MapUpdate.Type.PUT_IF_ABSENT)
353 .withKey("foo") 356 .withKey("foo")
...@@ -359,6 +362,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase { ...@@ -359,6 +362,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase {
359 map.prepare(tx).thenAccept(result -> { 362 map.prepare(tx).thenAccept(result -> {
360 assertEquals(true, result); 363 assertEquals(true, result);
361 }).join(); 364 }).join();
365 + // verify changes in Tx is not visible yet until commit
362 assertFalse(listener.eventReceived()); 366 assertFalse(listener.eventReceived());
363 367
364 map.size().thenAccept(result -> { 368 map.size().thenAccept(result -> {
...@@ -371,7 +375,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase { ...@@ -371,7 +375,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase {
371 375
372 try { 376 try {
373 map.put("foo", value2).join(); 377 map.put("foo", value2).join();
374 - assertTrue(false); 378 + fail("update to map entry in open tx should fail with Exception");
375 } catch (CompletionException e) { 379 } catch (CompletionException e) {
376 assertEquals(ConcurrentModificationException.class, e.getCause().getClass()); 380 assertEquals(ConcurrentModificationException.class, e.getCause().getClass());
377 } 381 }
...@@ -384,6 +388,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase { ...@@ -384,6 +388,7 @@ public class AtomixConsistentMapTest extends AtomixTestBase {
384 assertEquals(MapEvent.Type.INSERT, event.type()); 388 assertEquals(MapEvent.Type.INSERT, event.type());
385 assertTrue(Arrays.equals(value1, event.newValue().value())); 389 assertTrue(Arrays.equals(value1, event.newValue().value()));
386 390
391 + // map should be update-able after commit
387 map.put("foo", value2).thenAccept(result -> { 392 map.put("foo", value2).thenAccept(result -> {
388 assertTrue(Arrays.equals(Versioned.valueOrElse(result, null), value1)); 393 assertTrue(Arrays.equals(Versioned.valueOrElse(result, null), value1));
389 }).join(); 394 }).join();
...@@ -391,6 +396,43 @@ public class AtomixConsistentMapTest extends AtomixTestBase { ...@@ -391,6 +396,43 @@ public class AtomixConsistentMapTest extends AtomixTestBase {
391 assertNotNull(event); 396 assertNotNull(event);
392 assertEquals(MapEvent.Type.UPDATE, event.type()); 397 assertEquals(MapEvent.Type.UPDATE, event.type());
393 assertTrue(Arrays.equals(value2, event.newValue().value())); 398 assertTrue(Arrays.equals(value2, event.newValue().value()));
399 +
400 +
401 + // REMOVE_IF_VERSION_MATCH
402 + byte[] currFoo = map.get("foo").get().value();
403 + long currFooVersion = map.get("foo").get().version();
404 + MapUpdate<String, byte[]> remove1 =
405 + MapUpdate.<String, byte[]>newBuilder().withType(MapUpdate.Type.REMOVE_IF_VERSION_MATCH)
406 + .withKey("foo")
407 + .withCurrentVersion(currFooVersion)
408 + .build();
409 +
410 + tx = new MapTransaction<>(TransactionId.from("tx2"), Arrays.asList(remove1));
411 +
412 + map.prepare(tx).thenAccept(result -> {
413 + assertTrue("prepare should succeed", result);
414 + }).join();
415 + // verify changes in Tx is not visible yet until commit
416 + assertFalse(listener.eventReceived());
417 +
418 + map.size().thenAccept(size -> {
419 + assertThat(size, is(1));
420 + }).join();
421 +
422 + map.get("foo").thenAccept(result -> {
423 + assertThat(result.value(), is(currFoo));
424 + }).join();
425 +
426 + map.commit(tx.transactionId()).join();
427 + event = listener.event();
428 + assertNotNull(event);
429 + assertEquals(MapEvent.Type.REMOVE, event.type());
430 + assertArrayEquals(currFoo, event.oldValue().value());
431 +
432 + map.size().thenAccept(size -> {
433 + assertThat(size, is(0));
434 + }).join();
435 +
394 } 436 }
395 437
396 protected void transactionRollbackTests(int clusterSize) throws Throwable { 438 protected void transactionRollbackTests(int clusterSize) throws Throwable {
......
...@@ -45,10 +45,13 @@ public final class CountDownCompleter<T> { ...@@ -45,10 +45,13 @@ public final class CountDownCompleter<T> {
45 * @param onCompleteCallback callback to invoke when completer is completed 45 * @param onCompleteCallback callback to invoke when completer is completed
46 */ 46 */
47 public CountDownCompleter(T object, long count, Consumer<T> onCompleteCallback) { 47 public CountDownCompleter(T object, long count, Consumer<T> onCompleteCallback) {
48 - checkState(count > 0, "count must be positive"); 48 + checkState(count >= 0, "count must be non-negative");
49 this.counter = new AtomicLong(count); 49 this.counter = new AtomicLong(count);
50 this.object = checkNotNull(object); 50 this.object = checkNotNull(object);
51 this.onCompleteCallback = checkNotNull(onCompleteCallback); 51 this.onCompleteCallback = checkNotNull(onCompleteCallback);
52 + if (count == 0) {
53 + onCompleteCallback.accept(object);
54 + }
52 } 55 }
53 56
54 /** 57 /**
......