Jonathan Hart

Implement anti-entropy for the EventuallyConsistentMap.

ONOS-857. 

Change-Id: Ife2070142d3c165c2a0035c3011c05b426c8baa4
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +package org.onosproject.store.impl;
17 +
18 +import org.onosproject.cluster.NodeId;
19 +import org.onosproject.store.Timestamp;
20 +
21 +import java.util.Map;
22 +
23 +import static com.google.common.base.Preconditions.checkNotNull;
24 +
25 +/**
26 + * Anti-entropy advertisement message for eventually consistent map.
27 + */
28 +public class AntiEntropyAdvertisement<K> {
29 +
30 + private final NodeId sender;
31 + private final Map<K, Timestamp> timestamps;
32 + private final Map<K, Timestamp> tombstones;
33 +
34 + /**
35 + * Creates a new anti entropy advertisement message.
36 + *
37 + * @param sender the sender's node ID
38 + * @param timestamps map of item key to timestamp for current items
39 + * @param tombstones map of item key to timestamp for removed items
40 + */
41 + public AntiEntropyAdvertisement(NodeId sender,
42 + Map<K, Timestamp> timestamps,
43 + Map<K, Timestamp> tombstones) {
44 + this.sender = checkNotNull(sender);
45 + this.timestamps = checkNotNull(timestamps);
46 + this.tombstones = checkNotNull(tombstones);
47 + }
48 +
49 + /**
50 + * Returns the sender's node ID.
51 + *
52 + * @return the sender's node ID
53 + */
54 + public NodeId sender() {
55 + return sender;
56 + }
57 +
58 + /**
59 + * Returns the map of current item timestamps.
60 + *
61 + * @return current item timestamps
62 + */
63 + public Map<K, Timestamp> timestamps() {
64 + return timestamps;
65 + }
66 +
67 + /**
68 + * Returns the map of removed item timestamps.
69 + *
70 + * @return removed item timestamps
71 + */
72 + public Map<K, Timestamp> tombstones() {
73 + return tombstones;
74 + }
75 +
76 + // For serializer
77 + @SuppressWarnings("unused")
78 + private AntiEntropyAdvertisement() {
79 + this.sender = null;
80 + this.timestamps = null;
81 + this.tombstones = null;
82 + }
83 +}
...@@ -16,8 +16,10 @@ ...@@ -16,8 +16,10 @@
16 package org.onosproject.store.impl; 16 package org.onosproject.store.impl;
17 17
18 import com.google.common.base.MoreObjects; 18 import com.google.common.base.MoreObjects;
19 +import org.apache.commons.lang3.RandomUtils;
19 import org.onlab.util.KryoNamespace; 20 import org.onlab.util.KryoNamespace;
20 import org.onosproject.cluster.ClusterService; 21 import org.onosproject.cluster.ClusterService;
22 +import org.onosproject.cluster.ControllerNode;
21 import org.onosproject.cluster.NodeId; 23 import org.onosproject.cluster.NodeId;
22 import org.onosproject.store.Timestamp; 24 import org.onosproject.store.Timestamp;
23 import org.onosproject.store.cluster.messaging.ClusterCommunicationService; 25 import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
...@@ -32,6 +34,8 @@ import java.io.IOException; ...@@ -32,6 +34,8 @@ import java.io.IOException;
32 import java.util.ArrayList; 34 import java.util.ArrayList;
33 import java.util.Collection; 35 import java.util.Collection;
34 import java.util.Collections; 36 import java.util.Collections;
37 +import java.util.HashMap;
38 +import java.util.LinkedList;
35 import java.util.List; 39 import java.util.List;
36 import java.util.Map; 40 import java.util.Map;
37 import java.util.Set; 41 import java.util.Set;
...@@ -40,6 +44,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -40,6 +44,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
40 import java.util.concurrent.ExecutorService; 44 import java.util.concurrent.ExecutorService;
41 import java.util.concurrent.Executors; 45 import java.util.concurrent.Executors;
42 import java.util.concurrent.ScheduledExecutorService; 46 import java.util.concurrent.ScheduledExecutorService;
47 +import java.util.concurrent.TimeUnit;
43 import java.util.stream.Collectors; 48 import java.util.stream.Collectors;
44 49
45 import static com.google.common.base.Preconditions.checkNotNull; 50 import static com.google.common.base.Preconditions.checkNotNull;
...@@ -69,8 +74,9 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -69,8 +74,9 @@ public class EventuallyConsistentMapImpl<K, V>
69 74
70 private final MessageSubject updateMessageSubject; 75 private final MessageSubject updateMessageSubject;
71 private final MessageSubject removeMessageSubject; 76 private final MessageSubject removeMessageSubject;
77 + private final MessageSubject antiEntropyAdvertisementSubject;
72 78
73 - private final Set<EventuallyConsistentMapListener> listeners 79 + private final Set<EventuallyConsistentMapListener<K, V>> listeners
74 = new CopyOnWriteArraySet<>(); 80 = new CopyOnWriteArraySet<>();
75 81
76 private final ExecutorService executor; 82 private final ExecutorService executor;
...@@ -138,12 +144,20 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -138,12 +144,20 @@ public class EventuallyConsistentMapImpl<K, V>
138 newSingleThreadScheduledExecutor(minPriority( 144 newSingleThreadScheduledExecutor(minPriority(
139 namedThreads("onos-ecm-" + mapName + "-bg-%d"))); 145 namedThreads("onos-ecm-" + mapName + "-bg-%d")));
140 146
147 + // start anti-entropy thread
148 + backgroundExecutor.scheduleAtFixedRate(new SendAdvertisementTask(),
149 + initialDelaySec, periodSec,
150 + TimeUnit.SECONDS);
151 +
141 updateMessageSubject = new MessageSubject("ecm-" + mapName + "-update"); 152 updateMessageSubject = new MessageSubject("ecm-" + mapName + "-update");
142 clusterCommunicator.addSubscriber(updateMessageSubject, 153 clusterCommunicator.addSubscriber(updateMessageSubject,
143 new InternalPutEventListener()); 154 new InternalPutEventListener());
144 removeMessageSubject = new MessageSubject("ecm-" + mapName + "-remove"); 155 removeMessageSubject = new MessageSubject("ecm-" + mapName + "-remove");
145 clusterCommunicator.addSubscriber(removeMessageSubject, 156 clusterCommunicator.addSubscriber(removeMessageSubject,
146 new InternalRemoveEventListener()); 157 new InternalRemoveEventListener());
158 + antiEntropyAdvertisementSubject = new MessageSubject("ecm-" + mapName + "-anti-entropy");
159 + clusterCommunicator.addSubscriber(antiEntropyAdvertisementSubject,
160 + new InternalAntiEntropyListener());
147 } 161 }
148 162
149 private KryoSerializer createSerializer(KryoNamespace.Builder builder) { 163 private KryoSerializer createSerializer(KryoNamespace.Builder builder) {
...@@ -158,9 +172,9 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -158,9 +172,9 @@ public class EventuallyConsistentMapImpl<K, V>
158 .register(ArrayList.class) 172 .register(ArrayList.class)
159 .register(InternalPutEvent.class) 173 .register(InternalPutEvent.class)
160 .register(InternalRemoveEvent.class) 174 .register(InternalRemoveEvent.class)
175 + .register(AntiEntropyAdvertisement.class)
176 + .register(HashMap.class)
161 .build(); 177 .build();
162 -
163 - // TODO anti-entropy classes
164 } 178 }
165 }; 179 };
166 } 180 }
...@@ -360,8 +374,8 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -360,8 +374,8 @@ public class EventuallyConsistentMapImpl<K, V>
360 clusterCommunicator.removeSubscriber(removeMessageSubject); 374 clusterCommunicator.removeSubscriber(removeMessageSubject);
361 } 375 }
362 376
363 - private void notifyListeners(EventuallyConsistentMapEvent event) { 377 + private void notifyListeners(EventuallyConsistentMapEvent<K, V> event) {
364 - for (EventuallyConsistentMapListener listener : listeners) { 378 + for (EventuallyConsistentMapListener<K, V> listener : listeners) {
365 listener.event(event); 379 listener.event(event);
366 } 380 }
367 } 381 }
...@@ -418,6 +432,245 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -418,6 +432,245 @@ public class EventuallyConsistentMapImpl<K, V>
418 } 432 }
419 } 433 }
420 434
435 + private final class SendAdvertisementTask implements Runnable {
436 + @Override
437 + public void run() {
438 + if (Thread.currentThread().isInterrupted()) {
439 + log.info("Interrupted, quitting");
440 + return;
441 + }
442 +
443 + try {
444 + final NodeId self = clusterService.getLocalNode().id();
445 + Set<ControllerNode> nodes = clusterService.getNodes();
446 +
447 + List<NodeId> nodeIds = nodes.stream()
448 + .map(node -> node.id())
449 + .collect(Collectors.toList());
450 +
451 + if (nodeIds.size() == 1 && nodeIds.get(0).equals(self)) {
452 + log.trace("No other peers in the cluster.");
453 + return;
454 + }
455 +
456 + NodeId peer;
457 + do {
458 + int idx = RandomUtils.nextInt(0, nodeIds.size());
459 + peer = nodeIds.get(idx);
460 + } while (peer.equals(self));
461 +
462 + if (Thread.currentThread().isInterrupted()) {
463 + log.info("Interrupted, quitting");
464 + return;
465 + }
466 +
467 + AntiEntropyAdvertisement<K> ad = createAdvertisement();
468 +
469 + try {
470 + unicastMessage(peer, antiEntropyAdvertisementSubject, ad);
471 + } catch (IOException e) {
472 + log.debug("Failed to send anti-entropy advertisement to {}", peer);
473 + }
474 + } catch (Exception e) {
475 + // Catch all exceptions to avoid scheduled task being suppressed.
476 + log.error("Exception thrown while sending advertisement", e);
477 + }
478 + }
479 + }
480 +
481 + private AntiEntropyAdvertisement<K> createAdvertisement() {
482 + final NodeId self = clusterService.getLocalNode().id();
483 +
484 + Map<K, Timestamp> timestamps = new HashMap<>(items.size());
485 +
486 + items.forEach((key, value) -> timestamps.put(key, value.timestamp()));
487 +
488 + Map<K, Timestamp> tombstones = new HashMap<>(removedItems);
489 +
490 + return new AntiEntropyAdvertisement<>(self, timestamps, tombstones);
491 + }
492 +
493 + private void handleAntiEntropyAdvertisement(AntiEntropyAdvertisement<K> ad) {
494 + List<EventuallyConsistentMapEvent<K, V>> externalEvents;
495 +
496 + synchronized (this) {
497 + final NodeId sender = ad.sender();
498 +
499 + externalEvents = antiEntropyCheckLocalItems(ad);
500 +
501 + antiEntropyCheckLocalRemoved(ad);
502 +
503 + externalEvents.addAll(antiEntropyCheckRemoteRemoved(ad));
504 +
505 + // if remote ad has something unknown, actively sync
506 + for (K key : ad.timestamps().keySet()) {
507 + if (!items.containsKey(key)) {
508 + AntiEntropyAdvertisement<K> myAd = createAdvertisement();
509 + try {
510 + unicastMessage(sender, antiEntropyAdvertisementSubject,
511 + myAd);
512 + break;
513 + } catch (IOException e) {
514 + log.debug(
515 + "Failed to send reactive anti-entropy advertisement to {}",
516 + sender);
517 + }
518 + }
519 + }
520 + } // synchronized (this)
521 +
522 + externalEvents.forEach(this::notifyListeners);
523 + }
524 +
525 + /**
526 + * Checks if any of the remote's live items or tombstones are out of date
527 + * according to our local live item list, or if our live items are out of
528 + * date according to the remote's tombstone list.
529 + * If the local copy is more recent, it will be pushed to the remote. If the
530 + * remote has a more recent remove, we apply that to the local state.
531 + *
532 + * @param ad remote anti-entropy advertisement
533 + * @return list of external events relating to local operations performed
534 + */
535 + // Guarded by synchronized (this)
536 + private List<EventuallyConsistentMapEvent<K, V>> antiEntropyCheckLocalItems(
537 + AntiEntropyAdvertisement<K> ad) {
538 + final List<EventuallyConsistentMapEvent<K, V>> externalEvents
539 + = new LinkedList<>();
540 + final NodeId sender = ad.sender();
541 +
542 + final List<PutEntry<K, V>> updatesToSend = new ArrayList<>();
543 +
544 + for (Map.Entry<K, Timestamped<V>> item : items.entrySet()) {
545 + K key = item.getKey();
546 + Timestamped<V> localValue = item.getValue();
547 +
548 + Timestamp remoteTimestamp = ad.timestamps().get(key);
549 + if (remoteTimestamp == null) {
550 + remoteTimestamp = ad.tombstones().get(key);
551 + }
552 + if (remoteTimestamp == null || localValue
553 + .isNewer(remoteTimestamp)) {
554 + // local value is more recent, push to sender
555 + updatesToSend
556 + .add(new PutEntry<>(key, localValue.value(),
557 + localValue.timestamp()));
558 + }
559 +
560 + Timestamp remoteDeadTimestamp = ad.tombstones().get(key);
561 + if (remoteDeadTimestamp != null &&
562 + remoteDeadTimestamp.compareTo(localValue.timestamp()) > 0) {
563 + // sender has a more recent remove
564 + if (removeInternal(key, remoteDeadTimestamp)) {
565 + externalEvents.add(new EventuallyConsistentMapEvent<>(
566 + EventuallyConsistentMapEvent.Type.REMOVE, key, null));
567 + }
568 + }
569 + }
570 +
571 + // Send all updates to the peer at once
572 + if (!updatesToSend.isEmpty()) {
573 + try {
574 + unicastMessage(sender, updateMessageSubject, new InternalPutEvent<>(updatesToSend));
575 + } catch (IOException e) {
576 + log.warn("Failed to send advertisement response", e);
577 + }
578 + }
579 +
580 + return externalEvents;
581 + }
582 +
583 + /**
584 + * Checks if any items in the remote live list are out of date according
585 + * to our tombstone list. If we find we have a more up to date tombstone,
586 + * we'll send it to the remote.
587 + *
588 + * @param ad remote anti-entropy advertisement
589 + */
590 + // Guarded by synchronized (this)
591 + private void antiEntropyCheckLocalRemoved(AntiEntropyAdvertisement<K> ad) {
592 + final NodeId sender = ad.sender();
593 +
594 + final List<RemoveEntry<K>> removesToSend = new ArrayList<>();
595 +
596 + for (Map.Entry<K, Timestamp> dead : removedItems.entrySet()) {
597 + K key = dead.getKey();
598 + Timestamp localDeadTimestamp = dead.getValue();
599 +
600 + Timestamp remoteLiveTimestamp = ad.timestamps().get(key);
601 + if (remoteLiveTimestamp != null
602 + && localDeadTimestamp.compareTo(remoteLiveTimestamp) > 0) {
603 + // sender has zombie, push remove
604 + removesToSend
605 + .add(new RemoveEntry<>(key, localDeadTimestamp));
606 + }
607 + }
608 +
609 + // Send all removes to the peer at once
610 + if (!removesToSend.isEmpty()) {
611 + try {
612 + unicastMessage(sender, removeMessageSubject, new InternalRemoveEvent<>(removesToSend));
613 + } catch (IOException e) {
614 + log.warn("Failed to send advertisement response", e);
615 + }
616 + }
617 + }
618 +
619 + /**
620 + * Checks if any of the local live items are out of date according to the
621 + * remote's tombstone advertisements. If we find a local item is out of date,
622 + * we'll apply the remove operation to the local state.
623 + *
624 + * @param ad remote anti-entropy advertisement
625 + * @return list of external events relating to local operations performed
626 + */
627 + // Guarded by synchronized (this)
628 + private List<EventuallyConsistentMapEvent<K, V>>
629 + antiEntropyCheckRemoteRemoved(AntiEntropyAdvertisement<K> ad) {
630 + final List<EventuallyConsistentMapEvent<K, V>> externalEvents
631 + = new LinkedList<>();
632 +
633 + for (Map.Entry<K, Timestamp> remoteDead : ad.tombstones().entrySet()) {
634 + K key = remoteDead.getKey();
635 + Timestamp remoteDeadTimestamp = remoteDead.getValue();
636 +
637 + Timestamped<V> local = items.get(key);
638 + Timestamp localDead = removedItems.get(key);
639 + if (local != null
640 + && remoteDeadTimestamp.compareTo(local.timestamp()) > 0) {
641 + // remove our version
642 + if (removeInternal(key, remoteDeadTimestamp)) {
643 + externalEvents.add(new EventuallyConsistentMapEvent<>(
644 + EventuallyConsistentMapEvent.Type.REMOVE, key, null));
645 + }
646 + } else if (localDead != null &&
647 + remoteDeadTimestamp.compareTo(localDead) > 0) {
648 + // If we both had the item as removed, but their timestamp is
649 + // newer, update ours to the newer value
650 + removedItems.put(key, remoteDeadTimestamp);
651 + }
652 + }
653 +
654 + return externalEvents;
655 + }
656 +
657 + private final class InternalAntiEntropyListener
658 + implements ClusterMessageHandler {
659 +
660 + @Override
661 + public void handle(ClusterMessage message) {
662 + log.trace("Received anti-entropy advertisement from peer: {}", message.sender());
663 + AntiEntropyAdvertisement<K> advertisement = serializer.decode(message.payload());
664 + backgroundExecutor.submit(() -> {
665 + try {
666 + handleAntiEntropyAdvertisement(advertisement);
667 + } catch (Exception e) {
668 + log.warn("Exception thrown handling advertisements", e);
669 + }
670 + });
671 + }
672 + }
673 +
421 private final class InternalPutEventListener implements 674 private final class InternalPutEventListener implements
422 ClusterMessageHandler { 675 ClusterMessageHandler {
423 @Override 676 @Override
...@@ -433,7 +686,7 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -433,7 +686,7 @@ public class EventuallyConsistentMapImpl<K, V>
433 Timestamp timestamp = entry.timestamp(); 686 Timestamp timestamp = entry.timestamp();
434 687
435 if (putInternal(key, value, timestamp)) { 688 if (putInternal(key, value, timestamp)) {
436 - EventuallyConsistentMapEvent externalEvent = 689 + EventuallyConsistentMapEvent<K, V> externalEvent =
437 new EventuallyConsistentMapEvent<>( 690 new EventuallyConsistentMapEvent<>(
438 EventuallyConsistentMapEvent.Type.PUT, key, 691 EventuallyConsistentMapEvent.Type.PUT, key,
439 value); 692 value);
...@@ -461,7 +714,8 @@ public class EventuallyConsistentMapImpl<K, V> ...@@ -461,7 +714,8 @@ public class EventuallyConsistentMapImpl<K, V>
461 Timestamp timestamp = entry.timestamp(); 714 Timestamp timestamp = entry.timestamp();
462 715
463 if (removeInternal(key, timestamp)) { 716 if (removeInternal(key, timestamp)) {
464 - EventuallyConsistentMapEvent externalEvent = new EventuallyConsistentMapEvent<K, V>( 717 + EventuallyConsistentMapEvent<K, V> externalEvent
718 + = new EventuallyConsistentMapEvent<>(
465 EventuallyConsistentMapEvent.Type.REMOVE, 719 EventuallyConsistentMapEvent.Type.REMOVE,
466 key, null); 720 key, null);
467 notifyListeners(externalEvent); 721 notifyListeners(externalEvent);
......