Refactor flow rule store to resolve missing flows with old masters following mastership reassignment
Change-Id: I7b7e639c84cbd23fe9ab1f12080f865cdfc7f7f9
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BackupOperation.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BackupOperation.java
new file mode 100644
index 0000000..7034118
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BackupOperation.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.Objects;
+
+import org.onosproject.cluster.NodeId;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Identifier representing a backup of a distinct bucket to a specific node.
+ */
+public class BackupOperation {
+ private final NodeId nodeId;
+ private final int bucketId;
+
+ BackupOperation(NodeId nodeId, int bucketId) {
+ this.nodeId = nodeId;
+ this.bucketId = bucketId;
+ }
+
+ /**
+ * Returns the node identifier.
+ *
+ * @return the node identifier
+ */
+ public NodeId nodeId() {
+ return nodeId;
+ }
+
+ /**
+ * Returns the bucket identifier.
+ *
+ * @return the bucket identifier
+ */
+ public int bucket() {
+ return bucketId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nodeId, bucketId);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other != null && other instanceof BackupOperation) {
+ BackupOperation that = (BackupOperation) other;
+ return this.nodeId.equals(that.nodeId)
+ && this.bucketId == that.bucketId;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("nodeId", nodeId())
+ .add("bucket", bucket())
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BucketId.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BucketId.java
new file mode 100644
index 0000000..33cc304
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/BucketId.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.Objects;
+
+import org.onosproject.net.DeviceId;
+
+/**
+ * Represents a distinct device flow bucket.
+ */
+public class BucketId {
+ private final DeviceId deviceId;
+ private final int bucket;
+
+ BucketId(DeviceId deviceId, int bucket) {
+ this.deviceId = deviceId;
+ this.bucket = bucket;
+ }
+
+ /**
+ * Returns the bucket device identifier.
+ *
+ * @return the device identifier
+ */
+ public DeviceId deviceId() {
+ return deviceId;
+ }
+
+ /**
+ * Returns the bucket number.
+ *
+ * @return the bucket number
+ */
+ public int bucket() {
+ return bucket;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deviceId, bucket);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof BucketId) {
+ BucketId that = (BucketId) other;
+ return this.deviceId.equals(that.deviceId)
+ && this.bucket == that.bucket;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d", deviceId, bucket);
+ }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceFlowTable.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceFlowTable.java
new file mode 100644
index 0000000..6bc5b5f
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceFlowTable.java
@@ -0,0 +1,822 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.util.KryoNamespace;
+import org.onlab.util.Tools;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.StoredFlowEntry;
+import org.onosproject.store.LogicalTimestamp;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
+import org.onosproject.store.cluster.messaging.MessageSubject;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.Serializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Flow table for all flows associated with a specific device.
+ * <p>
+ * Flows in the table are stored in buckets. Each bucket is mutated and replicated as a single unit. The device flow
+ * table performs communication independent of other device flow tables for more parallelism.
+ * <p>
+ * This implementation uses several different replication protocols. Changes that occur on the device master are
+ * replicated to the backups provided in the {@link DeviceReplicaInfo} for the master's term. Additionally, a periodic
+ * anti-entropy protocol is used to detect missing flows on backups (e.g. due to a node restart). Finally, when a
+ * device mastership change occurs, the new master synchronizes flows with the prior master and/or backups for the
+ * device, allowing mastership to be reassigned to non-backup nodes.
+ */
+public class DeviceFlowTable {
+ private static final int NUM_BUCKETS = 1024;
+ private static final Serializer SERIALIZER = Serializer.using(KryoNamespace.newBuilder()
+ .register(KryoNamespaces.API)
+ .register(BucketId.class)
+ .register(FlowBucket.class)
+ .register(FlowBucketDigest.class)
+ .register(LogicalTimestamp.class)
+ .register(Timestamped.class)
+ .build());
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private final MessageSubject getDigestsSubject;
+ private final MessageSubject getBucketSubject;
+ private final MessageSubject backupSubject;
+
+ private final DeviceId deviceId;
+ private final ClusterCommunicationService clusterCommunicator;
+ private final LifecycleManager lifecycleManager;
+ private final ScheduledExecutorService executorService;
+ private final NodeId localNodeId;
+
+ private final LogicalClock clock = new LogicalClock();
+
+ private volatile DeviceReplicaInfo replicaInfo;
+ private volatile long activeTerm;
+
+ private final LifecycleEventListener lifecycleEventListener = new LifecycleEventListener() {
+ @Override
+ public void event(LifecycleEvent event) {
+ executorService.execute(() -> onLifecycleEvent(event));
+ }
+ };
+
+ private ScheduledFuture<?> backupFuture;
+ private ScheduledFuture<?> antiEntropyFuture;
+
+ private final Map<Integer, Queue<Runnable>> flowTasks = Maps.newConcurrentMap();
+ private final Map<Integer, FlowBucket> flowBuckets = Maps.newConcurrentMap();
+
+ private final Map<BackupOperation, LogicalTimestamp> lastBackupTimes = Maps.newConcurrentMap();
+ private final Set<BackupOperation> inFlightUpdates = Sets.newConcurrentHashSet();
+
+ DeviceFlowTable(
+ DeviceId deviceId,
+ ClusterService clusterService,
+ ClusterCommunicationService clusterCommunicator,
+ LifecycleManager lifecycleManager,
+ ScheduledExecutorService executorService,
+ long backupPeriod,
+ long antiEntropyPeriod) {
+ this.deviceId = deviceId;
+ this.clusterCommunicator = clusterCommunicator;
+ this.lifecycleManager = lifecycleManager;
+ this.executorService = executorService;
+ this.localNodeId = clusterService.getLocalNode().id();
+
+ addListeners();
+
+ for (int i = 0; i < NUM_BUCKETS; i++) {
+ flowBuckets.put(i, new FlowBucket(new BucketId(deviceId, i)));
+ }
+
+ getDigestsSubject = new MessageSubject(String.format("flow-store-%s-digests", deviceId));
+ getBucketSubject = new MessageSubject(String.format("flow-store-%s-bucket", deviceId));
+ backupSubject = new MessageSubject(String.format("flow-store-%s-backup", deviceId));
+
+ setBackupPeriod(backupPeriod);
+ setAntiEntropyPeriod(antiEntropyPeriod);
+ registerSubscribers();
+
+ startTerm(lifecycleManager.getReplicaInfo());
+ }
+
+ /**
+ * Sets the flow table backup period.
+ *
+ * @param backupPeriod the flow table backup period in milliseconds
+ */
+ synchronized void setBackupPeriod(long backupPeriod) {
+ ScheduledFuture<?> backupFuture = this.backupFuture;
+ if (backupFuture != null) {
+ backupFuture.cancel(false);
+ }
+ this.backupFuture = executorService.scheduleAtFixedRate(
+ this::backup, backupPeriod, backupPeriod, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Sets the flow table anti-entropy period.
+ *
+ * @param antiEntropyPeriod the flow table anti-entropy period in milliseconds
+ */
+ synchronized void setAntiEntropyPeriod(long antiEntropyPeriod) {
+ ScheduledFuture<?> antiEntropyFuture = this.antiEntropyFuture;
+ if (antiEntropyFuture != null) {
+ antiEntropyFuture.cancel(false);
+ }
+ this.antiEntropyFuture = executorService.scheduleAtFixedRate(
+ this::runAntiEntropy, antiEntropyPeriod, antiEntropyPeriod, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Counts the flows in the table.
+ *
+ * @return the total number of flows in the table
+ */
+ public int count() {
+ return flowBuckets.values().stream()
+ .mapToInt(FlowBucket::count)
+ .sum();
+ }
+
+ /**
+ * Returns the flow entry for the given rule.
+ *
+ * @param rule the rule for which to lookup the flow entry
+ * @return the flow entry for the given rule
+ */
+ public StoredFlowEntry getFlowEntry(FlowRule rule) {
+ return getBucket(rule.id())
+ .getFlowEntries(rule.id())
+ .get(rule);
+ }
+
+ /**
+ * Returns the set of flow entries in the table.
+ *
+ * @return the set of flow entries in the table
+ */
+ public Set<FlowEntry> getFlowEntries() {
+ return flowBuckets.values().stream()
+ .flatMap(bucket -> bucket.getFlowBucket().values().stream())
+ .flatMap(entries -> entries.values().stream())
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the bucket for the given flow identifier.
+ *
+ * @param flowId the flow identifier
+ * @return the bucket for the given flow identifier
+ */
+ private FlowBucket getBucket(FlowId flowId) {
+ return getBucket(bucket(flowId));
+ }
+
+ /**
+ * Returns the bucket with the given identifier.
+ *
+ * @param bucketId the bucket identifier
+ * @return the bucket with the given identifier
+ */
+ private FlowBucket getBucket(int bucketId) {
+ return flowBuckets.get(bucketId);
+ }
+
+ /**
+ * Returns the bucket number for the given flow identifier.
+ *
+ * @param flowId the flow identifier
+ * @return the bucket number for the given flow identifier
+ */
+ private int bucket(FlowId flowId) {
+ return Math.abs((int) (flowId.id() % NUM_BUCKETS));
+ }
+
+ /**
+ * Returns the digests for all buckets in the flow table for the device.
+ *
+ * @return the set of digests for all buckets for the device
+ */
+ private Set<FlowBucketDigest> getDigests() {
+ return flowBuckets.values()
+ .stream()
+ .map(bucket -> bucket.getDigest())
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Returns the digest for the given bucket.
+ *
+ * @param bucket the bucket for which to return the digest
+ * @return the digest for the given bucket
+ */
+ private FlowBucketDigest getDigest(int bucket) {
+ return flowBuckets.get(bucket).getDigest();
+ }
+
+ /**
+ * Adds an entry to the table.
+ *
+ * @param rule the rule to add
+ * @return a future to be completed once the rule has been added
+ */
+ public CompletableFuture<Void> add(FlowEntry rule) {
+ return runInTerm(rule.id(), (bucket, term) -> {
+ bucket.add(rule, term, clock);
+ return null;
+ });
+ }
+
+ /**
+ * Updates an entry in the table.
+ *
+ * @param rule the rule to update
+ * @return a future to be completed once the rule has been updated
+ */
+ public CompletableFuture<Void> update(FlowEntry rule) {
+ return runInTerm(rule.id(), (bucket, term) -> {
+ bucket.update(rule, term, clock);
+ return null;
+ });
+ }
+
+ /**
+ * Applies the given update function to the rule.
+ *
+ * @param rule the rule to update
+ * @param function the update function to apply
+ * @param <T> the result type
+ * @return a future to be completed with the update result or {@code null} if the rule was not updated
+ */
+ public <T> CompletableFuture<T> update(FlowRule rule, Function<StoredFlowEntry, T> function) {
+ return runInTerm(rule.id(), (bucket, term) -> bucket.update(rule, function, term, clock));
+ }
+
+ /**
+ * Removes an entry from the table.
+ *
+ * @param rule the rule to remove
+ * @return a future to be completed once the rule has been removed
+ */
+ public CompletableFuture<FlowEntry> remove(FlowEntry rule) {
+ return runInTerm(rule.id(), (bucket, term) -> bucket.remove(rule, term, clock));
+ }
+
+ /**
+ * Runs the given function in the current term.
+ *
+ * @param flowId the flow identifier indicating the bucket in which to run the function
+ * @param function the function to execute in the current term
+ * @param <T> the future result type
+ * @return a future to be completed with the function result once it has been run
+ */
+ private <T> CompletableFuture<T> runInTerm(FlowId flowId, BiFunction<FlowBucket, Long, T> function) {
+ DeviceReplicaInfo replicaInfo = lifecycleManager.getReplicaInfo();
+ if (!replicaInfo.isMaster(localNodeId)) {
+ return Tools.exceptionalFuture(new IllegalStateException());
+ }
+
+ FlowBucket bucket = getBucket(flowId);
+
+ // If the master's term is not currently active (has not been synchronized with prior replicas), enqueue
+ // the change to be executed once the master has been synchronized.
+ final long term = replicaInfo.term();
+ if (activeTerm < term) {
+ log.debug("Enqueueing operation for device {}", deviceId);
+ synchronized (flowTasks) {
+ // Double checked lock on the active term.
+ if (activeTerm < term) {
+ CompletableFuture<T> future = new CompletableFuture<>();
+ flowTasks.computeIfAbsent(bucket.bucketId().bucket(), b -> new LinkedList<>())
+ .add(() -> future.complete(function.apply(bucket, term)));
+ return future;
+ }
+ }
+ }
+ return CompletableFuture.completedFuture(function.apply(bucket, term));
+ }
+
+ /**
+ * Backs up all buckets in the given device to the given node.
+ */
+ private void backup() {
+ DeviceReplicaInfo replicaInfo = lifecycleManager.getReplicaInfo();
+
+ // If the local node is not currently the master, skip the backup.
+ if (!replicaInfo.isMaster(localNodeId)) {
+ return;
+ }
+
+ // Otherwise, iterate through backup nodes and backup the device.
+ for (NodeId nodeId : replicaInfo.backups()) {
+ try {
+ backup(nodeId, replicaInfo.term());
+ } catch (Exception e) {
+ log.error("Backup of " + deviceId + " to " + nodeId + " failed", e);
+ }
+ }
+ }
+
+ /**
+ * Backs up all buckets for the device to the given node.
+ *
+ * @param nodeId the node to which to back up the device
+ * @param term the term for which to backup to the node
+ */
+ private void backup(NodeId nodeId, long term) {
+ for (FlowBucket bucket : flowBuckets.values()) {
+ // If the bucket is not in the current term, skip it. This forces synchronization of the bucket
+ // to occur prior to the new master replicating changes in the bucket to backups.
+ if (bucket.term() != term) {
+ continue;
+ }
+
+ // Record the logical timestamp from the bucket to keep track of the highest logical time replicated.
+ LogicalTimestamp timestamp = bucket.timestamp();
+
+ // If the backup can be run (no concurrent backup to the node in progress) then run it.
+ BackupOperation operation = new BackupOperation(nodeId, bucket.bucketId().bucket());
+ if (startBackup(operation, timestamp)) {
+ backup(bucket, nodeId).whenCompleteAsync((succeeded, error) -> {
+ if (error != null) {
+ log.debug("Backup operation {} failed", operation, error);
+ failBackup(operation);
+ } else if (succeeded) {
+ succeedBackup(operation, timestamp);
+ backup(nodeId, term);
+ } else {
+ log.debug("Backup operation {} failed: term mismatch", operation);
+ failBackup(operation);
+ }
+ }, executorService);
+ }
+ }
+ }
+
+ /**
+ * Returns a boolean indicating whether the given {@link BackupOperation} can be started.
+ * <p>
+ * The backup can be started if no backup for the same device/bucket/node is already in progress and changes
+ * are pending replication for the backup operation.
+ *
+ * @param operation the operation to start
+ * @param timestamp the timestamp for which to start the backup operation
+ * @return indicates whether the given backup operation should be started
+ */
+ private boolean startBackup(BackupOperation operation, LogicalTimestamp timestamp) {
+ LogicalTimestamp lastBackupTime = lastBackupTimes.get(operation);
+ return timestamp != null
+ && (lastBackupTime == null || lastBackupTime.isOlderThan(timestamp))
+ && inFlightUpdates.add(operation);
+ }
+
+ /**
+ * Fails the given backup operation.
+ *
+ * @param operation the backup operation to fail
+ */
+ private void failBackup(BackupOperation operation) {
+ inFlightUpdates.remove(operation);
+ }
+
+ /**
+ * Succeeds the given backup operation.
+ * <p>
+ * The last backup time for the operation will be updated and the operation will be removed from
+ * in-flight updates.
+ *
+ * @param operation the operation to succeed
+ * @param timestamp the timestamp at which the operation was <em>started</em>
+ */
+ private void succeedBackup(BackupOperation operation, LogicalTimestamp timestamp) {
+ lastBackupTimes.put(operation, timestamp);
+ inFlightUpdates.remove(operation);
+ }
+
+ /**
+ * Resets the last completion time for the given backup operation to ensure it's replicated again.
+ *
+ * @param operation the backup operation to reset
+ */
+ private void resetBackup(BackupOperation operation) {
+ lastBackupTimes.remove(operation);
+ }
+
+ /**
+ * Performs the given backup operation.
+ *
+ * @param bucket the bucket to backup
+ * @param nodeId the node to which to backup the bucket
+ * @return a future to be completed with a boolean indicating whether the backup operation was successful
+ */
+ private CompletableFuture<Boolean> backup(FlowBucket bucket, NodeId nodeId) {
+ if (log.isDebugEnabled()) {
+ log.debug("Backing up {} flow entries in bucket {} to {}", bucket.count(), bucket.bucketId(), nodeId);
+ }
+ return sendWithTimestamp(bucket, backupSubject, nodeId);
+ }
+
+ /**
+ * Handles a flow bucket backup from a remote peer.
+ *
+ * @param flowBucket the flow bucket to back up
+ * @return the set of flows that could not be backed up
+ */
+ private boolean onBackup(FlowBucket flowBucket) {
+ if (log.isDebugEnabled()) {
+ log.debug("{} - Received {} flow entries in bucket {} to backup",
+ deviceId, flowBucket.count(), flowBucket.bucketId());
+ }
+
+ try {
+ DeviceReplicaInfo replicaInfo = lifecycleManager.getReplicaInfo();
+
+ // If the backup is for a different term, reject the request until we learn about the new term.
+ if (flowBucket.term() != replicaInfo.term()) {
+ log.debug("Term mismatch for device {}: {} != {}", deviceId, flowBucket.term(), replicaInfo);
+ return false;
+ }
+
+ flowBuckets.compute(flowBucket.bucketId().bucket(),
+ (id, bucket) -> flowBucket.getDigest().isNewerThan(bucket.getDigest()) ? flowBucket : bucket);
+ return true;
+ } catch (Exception e) {
+ log.warn("Failure processing backup request", e);
+ return false;
+ }
+ }
+
+ /**
+ * Runs the anti-entropy protocol.
+ */
+ private void runAntiEntropy() {
+ DeviceReplicaInfo replicaInfo = lifecycleManager.getReplicaInfo();
+ if (!replicaInfo.isMaster(localNodeId)) {
+ return;
+ }
+
+ for (NodeId nodeId : replicaInfo.backups()) {
+ runAntiEntropy(nodeId);
+ }
+ }
+
+ /**
+ * Runs the anti-entropy protocol against the given peer.
+ *
+ * @param nodeId the node with which to execute the anti-entropy protocol
+ */
+ private void runAntiEntropy(NodeId nodeId) {
+ requestDigests(nodeId).thenAcceptAsync((digests) -> {
+ // Compute a set of missing BucketIds based on digest times and send them back to the master.
+ for (FlowBucketDigest remoteDigest : digests) {
+ FlowBucket localBucket = getBucket(remoteDigest.bucket());
+ if (localBucket.getDigest().isNewerThan(remoteDigest)) {
+ log.debug("Detected missing flow entries on node {} in bucket {}/{}",
+ nodeId, deviceId, remoteDigest.bucket());
+ resetBackup(new BackupOperation(nodeId, remoteDigest.bucket()));
+ }
+ }
+ }, executorService);
+ }
+
+ /**
+ * Sends a digest request to the given node.
+ *
+ * @param nodeId the node to which to send the request
+ * @return future to be completed with the set of digests for the given device on the given node
+ */
+ private CompletableFuture<Set<FlowBucketDigest>> requestDigests(NodeId nodeId) {
+ return sendWithTimestamp(deviceId, getDigestsSubject, nodeId);
+ }
+
+ /**
+ * Synchronizes flows from the previous master or backups.
+ *
+ * @param prevReplicaInfo the previous replica info
+ * @param newReplicaInfo the new replica info
+ */
+ private void syncFlows(DeviceReplicaInfo prevReplicaInfo, DeviceReplicaInfo newReplicaInfo) {
+ if (prevReplicaInfo == null) {
+ activateMaster(newReplicaInfo);
+ } else if (prevReplicaInfo.master() != null && !prevReplicaInfo.master().equals(localNodeId)) {
+ syncFlowsOnMaster(prevReplicaInfo, newReplicaInfo);
+ } else {
+ syncFlowsOnBackups(prevReplicaInfo, newReplicaInfo);
+ }
+ }
+
+ /**
+ * Synchronizes flows from the previous master, falling back to backups if the master fails.
+ *
+ * @param prevReplicaInfo the previous replica info
+ * @param newReplicaInfo the new replica info
+ */
+ private void syncFlowsOnMaster(DeviceReplicaInfo prevReplicaInfo, DeviceReplicaInfo newReplicaInfo) {
+ syncFlowsOn(prevReplicaInfo.master())
+ .whenCompleteAsync((result, error) -> {
+ if (error != null) {
+ log.debug("Failed to synchronize flows on previous master {}", prevReplicaInfo.master(), error);
+ syncFlowsOnBackups(prevReplicaInfo, newReplicaInfo);
+ } else {
+ activateMaster(newReplicaInfo);
+ }
+ }, executorService);
+ }
+
+ /**
+ * Synchronizes flows from the previous backups.
+ *
+ * @param prevReplicaInfo the previous replica info
+ * @param newReplicaInfo the new replica info
+ */
+ private void syncFlowsOnBackups(DeviceReplicaInfo prevReplicaInfo, DeviceReplicaInfo newReplicaInfo) {
+ List<NodeId> backups = prevReplicaInfo.backups()
+ .stream()
+ .filter(nodeId -> !nodeId.equals(localNodeId))
+ .collect(Collectors.toList());
+ syncFlowsOn(backups)
+ .whenCompleteAsync((result, error) -> {
+ if (error != null) {
+ log.debug("Failed to synchronize flows on previous backup nodes {}", backups, error);
+ }
+ activateMaster(newReplicaInfo);
+ }, executorService);
+ }
+
+ /**
+ * Synchronizes flows for the device on the given nodes.
+ *
+ * @param nodes the nodes via which to synchronize the flows
+ * @return a future to be completed once flows have been synchronizes
+ */
+ private CompletableFuture<Void> syncFlowsOn(Collection<NodeId> nodes) {
+ return nodes.isEmpty()
+ ? CompletableFuture.completedFuture(null)
+ : Tools.firstOf(nodes.stream()
+ .map(node -> syncFlowsOn(node))
+ .collect(Collectors.toList()))
+ .thenApply(v -> null);
+ }
+
+ /**
+ * Synchronizes flows for the device from the given node.
+ *
+ * @param nodeId the node from which to synchronize flows
+ * @return a future to be completed once the flows have been synchronizes
+ */
+ private CompletableFuture<Void> syncFlowsOn(NodeId nodeId) {
+ return requestDigests(nodeId)
+ .thenCompose(digests -> Tools.allOf(digests.stream()
+ .filter(digest -> digest.isNewerThan(getDigest(digest.bucket())))
+ .map(digest -> syncBucketOn(nodeId, digest.bucket()))
+ .collect(Collectors.toList())))
+ .thenApply(v -> null);
+ }
+
+ /**
+ * Synchronizes the given bucket on the given node.
+ *
+ * @param nodeId the node on which to synchronize the bucket
+ * @param bucketNumber the bucket to synchronize
+ * @return a future to be completed once the bucket has been synchronizes
+ */
+ private CompletableFuture<Void> syncBucketOn(NodeId nodeId, int bucketNumber) {
+ return requestBucket(nodeId, bucketNumber)
+ .thenAcceptAsync(flowBucket -> {
+ flowBuckets.compute(flowBucket.bucketId().bucket(),
+ (id, bucket) -> flowBucket.getDigest().isNewerThan(bucket.getDigest()) ? flowBucket : bucket);
+ }, executorService);
+ }
+
+ /**
+ * Requests the given bucket from the given node.
+ *
+ * @param nodeId the node from which to request the bucket
+ * @param bucket the bucket to request
+ * @return a future to be completed with the bucket
+ */
+ private CompletableFuture<FlowBucket> requestBucket(NodeId nodeId, int bucket) {
+ log.debug("Requesting flow bucket {} from {}", bucket, nodeId);
+ return sendWithTimestamp(bucket, getBucketSubject, nodeId);
+ }
+
+ /**
+ * Handles a flow bucket request.
+ *
+ * @param bucket the bucket number
+ * @return the flow bucket
+ */
+ private FlowBucket onGetBucket(int bucket) {
+ return flowBuckets.get(bucket);
+ }
+
+ /**
+ * Activates the new master term.
+ *
+ * @param replicaInfo the new replica info
+ */
+ private void activateMaster(DeviceReplicaInfo replicaInfo) {
+ log.debug("Activating term {} for device {}", replicaInfo.term(), deviceId);
+ for (int i = 0; i < NUM_BUCKETS; i++) {
+ activateBucket(i);
+ }
+ lifecycleManager.activate(replicaInfo.term());
+ activeTerm = replicaInfo.term();
+ }
+
+ /**
+ * Activates the given bucket number.
+ *
+ * @param bucket the bucket number to activate
+ */
+ private void activateBucket(int bucket) {
+ Queue<Runnable> tasks;
+ synchronized (flowTasks) {
+ tasks = flowTasks.remove(bucket);
+ }
+ if (tasks != null) {
+ log.debug("Completing enqueued operations for device {}", deviceId);
+ tasks.forEach(task -> task.run());
+ }
+ }
+
+ /**
+ * Handles a lifecycle event.
+ */
+ private void onLifecycleEvent(LifecycleEvent event) {
+ log.debug("Received lifecycle event for device {}: {}", deviceId, event);
+ switch (event.type()) {
+ case TERM_START:
+ startTerm(event.subject());
+ break;
+ case TERM_ACTIVE:
+ activateTerm(event.subject());
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Handles a replica change at the start of a new term.
+ */
+ private void startTerm(DeviceReplicaInfo replicaInfo) {
+ DeviceReplicaInfo oldReplicaInfo = this.replicaInfo;
+ this.replicaInfo = replicaInfo;
+ if (replicaInfo.isMaster(localNodeId)) {
+ log.info("Synchronizing device {} flows for term {}", deviceId, replicaInfo.term());
+ syncFlows(oldReplicaInfo, replicaInfo);
+ }
+ }
+
+ /**
+ * Handles the activation of a term.
+ */
+ private void activateTerm(DeviceReplicaInfo replicaInfo) {
+ if (replicaInfo.term() < this.replicaInfo.term()) {
+ return;
+ }
+ if (replicaInfo.term() > this.replicaInfo.term()) {
+ this.replicaInfo = replicaInfo;
+ }
+
+ // If the local node is neither the master or a backup for the device, clear the flow table.
+ if (!replicaInfo.isMaster(localNodeId) && !replicaInfo.isBackup(localNodeId)) {
+ flowBuckets.values().forEach(bucket -> bucket.clear());
+ }
+ activeTerm = replicaInfo.term();
+ }
+
+ /**
+ * Sends a message to the given node wrapped in a Lamport timestamp.
+ * <p>
+ * Messages are sent in a {@link Timestamped} wrapper and are expected to be received in a {@link Timestamped}
+ * wrapper. The internal {@link LogicalClock} is automatically updated on both send and receive.
+ *
+ * @param message the message to send
+ * @param subject the message subject
+ * @param toNodeId the node to which to send the message
+ * @param <M> the message type
+ * @param <R> the response type
+ * @return a future to be completed with the response
+ */
+ private <M, R> CompletableFuture<R> sendWithTimestamp(M message, MessageSubject subject, NodeId toNodeId) {
+ return clusterCommunicator.<Timestamped<M>, Timestamped<R>>sendAndReceive(
+ clock.timestamp(message), subject, SERIALIZER::encode, SERIALIZER::decode, toNodeId)
+ .thenApply(response -> {
+ clock.tick(response.timestamp());
+ return response.value();
+ });
+ }
+
+ /**
+ * Receives messages to the given subject wrapped in Lamport timestamps.
+ * <p>
+ * Messages are expected to be received in a {@link Timestamped} wrapper and are sent back in a {@link Timestamped}
+ * wrapper. The internal {@link LogicalClock} is automatically updated on both receive and send.
+ *
+ * @param subject the subject for which to register the subscriber
+ * @param function the raw message handler
+ * @param <M> the raw message type
+ * @param <R> the raw response type
+ */
+ private <M, R> void receiveWithTimestamp(MessageSubject subject, Function<M, R> function) {
+ clusterCommunicator.<Timestamped<M>, Timestamped<R>>addSubscriber(subject, SERIALIZER::decode, request -> {
+ clock.tick(request.timestamp());
+ return clock.timestamp(function.apply(request.value()));
+ }, SERIALIZER::encode, executorService);
+ }
+
+ /**
+ * Registers internal message subscribers.
+ */
+ private void registerSubscribers() {
+ receiveWithTimestamp(getDigestsSubject, v -> getDigests());
+ receiveWithTimestamp(getBucketSubject, this::onGetBucket);
+ receiveWithTimestamp(backupSubject, this::onBackup);
+ }
+
+ /**
+ * Unregisters internal message subscribers.
+ */
+ private void unregisterSubscribers() {
+ clusterCommunicator.removeSubscriber(getDigestsSubject);
+ clusterCommunicator.removeSubscriber(getBucketSubject);
+ clusterCommunicator.removeSubscriber(backupSubject);
+ }
+
+ /**
+ * Adds internal event listeners.
+ */
+ private void addListeners() {
+ lifecycleManager.addListener(lifecycleEventListener);
+ }
+
+ /**
+ * Removes internal event listeners.
+ */
+ private void removeListeners() {
+ lifecycleManager.removeListener(lifecycleEventListener);
+ }
+
+ /**
+ * Cancels recurrent scheduled futures.
+ */
+ private synchronized void cancelFutures() {
+ ScheduledFuture<?> backupFuture = this.backupFuture;
+ if (backupFuture != null) {
+ backupFuture.cancel(false);
+ }
+
+ ScheduledFuture<?> antiEntropyFuture = this.antiEntropyFuture;
+ if (antiEntropyFuture != null) {
+ antiEntropyFuture.cancel(false);
+ }
+ }
+
+ /**
+ * Closes the device flow table.
+ */
+ public void close() {
+ removeListeners();
+ unregisterSubscribers();
+ cancelFutures();
+ lifecycleManager.close();
+ }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceReplicaInfo.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceReplicaInfo.java
new file mode 100644
index 0000000..30f9396
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/DeviceReplicaInfo.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.onosproject.cluster.NodeId;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Device term context.
+ */
+public class DeviceReplicaInfo {
+ private final long term;
+ private final NodeId master;
+ private final List<NodeId> backups;
+
+ public DeviceReplicaInfo(long term, NodeId master, List<NodeId> backups) {
+ this.term = term;
+ this.master = master;
+ this.backups = backups;
+ }
+
+ /**
+ * Returns the mastership term.
+ *
+ * @return the mastership term
+ */
+ public long term() {
+ return term;
+ }
+
+ /**
+ * Returns the master for the {@link #term()}.
+ *
+ * @return the master {@link NodeId} for the current {@link #term()}
+ */
+ public NodeId master() {
+ return master;
+ }
+
+ /**
+ * Returns a boolean indicating whether the given {@link NodeId} is the current master.
+ *
+ * @param nodeId the node ID to check
+ * @return indicates whether the given node identifier is the identifier of the current master
+ */
+ public boolean isMaster(NodeId nodeId) {
+ return Objects.equals(master, nodeId);
+ }
+
+ /**
+ * Returns a list of all active backup nodes in priority order.
+ * <p>
+ * The returned backups are limited by the flow rule store's configured backup count.
+ *
+ * @return a list of backup nodes in priority order
+ */
+ public List<NodeId> backups() {
+ return backups;
+ }
+
+ /**
+ * Returns a boolean indicating whether the given node is a backup.
+ *
+ * @param nodeId the node identifier
+ * @return indicates whether the given node is a backup
+ */
+ public boolean isBackup(NodeId nodeId) {
+ return backups.contains(nodeId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(term, master, backups);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof DeviceReplicaInfo) {
+ DeviceReplicaInfo that = (DeviceReplicaInfo) object;
+ return this.term == that.term
+ && Objects.equals(this.master, that.master)
+ && Objects.equals(this.backups, that.backups);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("term", term())
+ .add("master", master())
+ .add("backups", backups())
+ .toString();
+ }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
index 2919f08..19bfd48 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
@@ -1,44 +1,38 @@
- /*
- * Copyright 2014-present Open Networking Foundation
- *
- * 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.
- */
+/*
+* Copyright 2014-present Open Networking Foundation
+*
+* 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 org.onosproject.store.flow.impl;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
-import com.google.common.util.concurrent.Futures;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
@@ -54,14 +48,16 @@
import org.onosproject.cluster.NodeId;
import org.onosproject.core.CoreService;
import org.onosproject.core.IdGenerator;
+import org.onosproject.event.AbstractListenerManager;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.CompletedBatchOperation;
import org.onosproject.net.flow.DefaultFlowEntry;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.FlowEntry.FlowEntryState;
-import org.onosproject.net.flow.FlowId;
import org.onosproject.net.flow.FlowRule;
import org.onosproject.net.flow.FlowRuleEvent;
import org.onosproject.net.flow.FlowRuleEvent.Type;
@@ -80,14 +76,18 @@
import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
import org.onosproject.store.cluster.messaging.ClusterMessage;
import org.onosproject.store.cluster.messaging.ClusterMessageHandler;
+import org.onosproject.store.flow.ReplicaInfo;
import org.onosproject.store.flow.ReplicaInfoEvent;
import org.onosproject.store.flow.ReplicaInfoEventListener;
import org.onosproject.store.flow.ReplicaInfoService;
import org.onosproject.store.impl.MastershipBasedTimestamp;
import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.AsyncConsistentMap;
import org.onosproject.store.service.EventuallyConsistentMap;
import org.onosproject.store.service.EventuallyConsistentMapEvent;
import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
import org.onosproject.store.service.Serializer;
import org.onosproject.store.service.StorageService;
import org.onosproject.store.service.WallClockTimestamp;
@@ -99,8 +99,8 @@
import static org.onlab.util.Tools.groupedThreads;
import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.APPLY_BATCH_FLOWS;
-import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.FLOW_TABLE_ANTI_ENTROPY;
import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.FLOW_TABLE_BACKUP;
+import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.GET_DEVICE_FLOW_COUNT;
import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.GET_DEVICE_FLOW_ENTRIES;
import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.GET_FLOW_ENTRY;
import static org.onosproject.store.flow.impl.ECFlowRuleStoreMessageSubjects.REMOTE_APPLY_COMPLETED;
@@ -113,8 +113,8 @@
@Component(immediate = true)
@Service
public class ECFlowRuleStore
- extends AbstractStore<FlowRuleBatchEvent, FlowRuleStoreDelegate>
- implements FlowRuleStore {
+ extends AbstractStore<FlowRuleBatchEvent, FlowRuleStoreDelegate>
+ implements FlowRuleStore {
private final Logger log = getLogger(getClass());
@@ -124,26 +124,25 @@
private static final int DEFAULT_BACKUP_PERIOD_MILLIS = 2000;
private static final int DEFAULT_ANTI_ENTROPY_PERIOD_MILLIS = 5000;
private static final long FLOW_RULE_STORE_TIMEOUT_MILLIS = 5000;
- private static final int NUM_BUCKETS = 1024;
@Property(name = "msgHandlerPoolSize", intValue = MESSAGE_HANDLER_THREAD_POOL_SIZE,
- label = "Number of threads in the message handler pool")
+ label = "Number of threads in the message handler pool")
private int msgHandlerPoolSize = MESSAGE_HANDLER_THREAD_POOL_SIZE;
@Property(name = "backupPeriod", intValue = DEFAULT_BACKUP_PERIOD_MILLIS,
- label = "Delay in ms between successive backup runs")
+ label = "Delay in ms between successive backup runs")
private int backupPeriod = DEFAULT_BACKUP_PERIOD_MILLIS;
@Property(name = "antiEntropyPeriod", intValue = DEFAULT_ANTI_ENTROPY_PERIOD_MILLIS,
- label = "Delay in ms between anti-entropy runs")
+ label = "Delay in ms between anti-entropy runs")
private int antiEntropyPeriod = DEFAULT_ANTI_ENTROPY_PERIOD_MILLIS;
@Property(name = "persistenceEnabled", boolValue = false,
- label = "Indicates whether or not changes in the flow table should be persisted to disk.")
+ label = "Indicates whether or not changes in the flow table should be persisted to disk.")
private boolean persistenceEnabled = DEFAULT_PERSISTENCE_ENABLED;
@Property(name = "backupCount", intValue = DEFAULT_MAX_BACKUP_COUNT,
- label = "Max number of backup copies for each device")
+ label = "Max number of backup copies for each device")
private volatile int backupCount = DEFAULT_MAX_BACKUP_COUNT;
private InternalFlowTable flowTable = new InternalFlowTable();
@@ -176,14 +175,12 @@
private ExecutorService messageHandlingExecutor;
private ExecutorService eventHandler;
- private ScheduledFuture<?> backupTask;
- private ScheduledFuture<?> antiEntropyTask;
private final ScheduledExecutorService backupSenderExecutor =
- Executors.newSingleThreadScheduledExecutor(groupedThreads("onos/flow", "backup-sender", log));
+ Executors.newSingleThreadScheduledExecutor(groupedThreads("onos/flow", "backup-sender", log));
private EventuallyConsistentMap<DeviceId, List<TableStatisticsEntry>> deviceTableStats;
private final EventuallyConsistentMapListener<DeviceId, List<TableStatisticsEntry>> tableStatsListener =
- new InternalTableStatsListener();
+ new InternalTableStatsListener();
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected StorageService storageService;
@@ -199,7 +196,7 @@
.register(BucketId.class)
.register(MastershipBasedTimestamp.class);
- private EventuallyConsistentMap<BucketId, Integer> flowCounts;
+ protected AsyncConsistentMap<DeviceId, Long> mastershipTermLifecycles;
private IdGenerator idGenerator;
private NodeId local;
@@ -213,50 +210,37 @@
local = clusterService.getLocalNode().id();
eventHandler = Executors.newSingleThreadExecutor(
- groupedThreads("onos/flow", "event-handler", log));
+ groupedThreads("onos/flow", "event-handler", log));
messageHandlingExecutor = Executors.newFixedThreadPool(
- msgHandlerPoolSize, groupedThreads("onos/store/flow", "message-handlers", log));
+ msgHandlerPoolSize, groupedThreads("onos/store/flow", "message-handlers", log));
registerMessageHandlers(messageHandlingExecutor);
- replicaInfoManager.addListener(flowTable);
- backupTask = backupSenderExecutor.scheduleWithFixedDelay(
- flowTable::backup,
- 0,
- backupPeriod,
- TimeUnit.MILLISECONDS);
- antiEntropyTask = backupSenderExecutor.scheduleWithFixedDelay(
- flowTable::runAntiEntropy,
- 0,
- antiEntropyPeriod,
- TimeUnit.MILLISECONDS);
-
- flowCounts = storageService.<BucketId, Integer>eventuallyConsistentMapBuilder()
- .withName("onos-flow-counts")
- .withSerializer(serializerBuilder)
- .withAntiEntropyPeriod(5, TimeUnit.SECONDS)
- .withTimestampProvider((k, v) -> new WallClockTimestamp())
- .withTombstonesDisabled()
- .build();
+ mastershipTermLifecycles = storageService.<DeviceId, Long>consistentMapBuilder()
+ .withName("onos-flow-store-terms")
+ .withSerializer(serializer)
+ .buildAsyncMap();
deviceTableStats = storageService.<DeviceId, List<TableStatisticsEntry>>eventuallyConsistentMapBuilder()
- .withName("onos-flow-table-stats")
- .withSerializer(serializerBuilder)
- .withAntiEntropyPeriod(5, TimeUnit.SECONDS)
- .withTimestampProvider((k, v) -> new WallClockTimestamp())
- .withTombstonesDisabled()
- .build();
+ .withName("onos-flow-table-stats")
+ .withSerializer(serializerBuilder)
+ .withAntiEntropyPeriod(5, TimeUnit.SECONDS)
+ .withTimestampProvider((k, v) -> new WallClockTimestamp())
+ .withTombstonesDisabled()
+ .build();
deviceTableStats.addListener(tableStatsListener);
+ deviceService.addListener(flowTable);
+ deviceService.getDevices().forEach(device -> flowTable.addDevice(device.id()));
+
logConfig("Started");
}
@Deactivate
public void deactivate(ComponentContext context) {
- replicaInfoManager.removeListener(flowTable);
- backupTask.cancel(true);
configService.unregisterProperties(getClass(), false);
unregisterMessageHandlers();
+ deviceService.removeListener(flowTable);
deviceTableStats.removeListener(tableStatsListener);
deviceTableStats.destroy();
eventHandler.shutdownNow();
@@ -297,48 +281,21 @@
newAntiEntropyPeriod = DEFAULT_ANTI_ENTROPY_PERIOD_MILLIS;
}
- boolean restartBackupTask = false;
- boolean restartAntiEntropyTask = false;
-
if (newBackupPeriod != backupPeriod) {
backupPeriod = newBackupPeriod;
- restartBackupTask = true;
+ flowTable.setBackupPeriod(newBackupPeriod);
}
if (newAntiEntropyPeriod != antiEntropyPeriod) {
antiEntropyPeriod = newAntiEntropyPeriod;
- restartAntiEntropyTask = true;
- }
-
- if (restartBackupTask) {
- if (backupTask != null) {
- // cancel previously running task
- backupTask.cancel(false);
- }
- backupTask = backupSenderExecutor.scheduleWithFixedDelay(
- flowTable::backup,
- 0,
- backupPeriod,
- TimeUnit.MILLISECONDS);
- }
-
- if (restartAntiEntropyTask) {
- if (antiEntropyTask != null) {
- // cancel previously running task
- antiEntropyTask.cancel(false);
- }
- antiEntropyTask = backupSenderExecutor.scheduleWithFixedDelay(
- flowTable::runAntiEntropy,
- 0,
- antiEntropyPeriod,
- TimeUnit.MILLISECONDS);
+ flowTable.setAntiEntropyPeriod(newAntiEntropyPeriod);
}
if (newPoolSize != msgHandlerPoolSize) {
msgHandlerPoolSize = newPoolSize;
ExecutorService oldMsgHandler = messageHandlingExecutor;
messageHandlingExecutor = Executors.newFixedThreadPool(
- msgHandlerPoolSize, groupedThreads("onos/store/flow", "message-handlers", log));
+ msgHandlerPoolSize, groupedThreads("onos/store/flow", "message-handlers", log));
// replace previously registered handlers.
registerMessageHandlers(messageHandlingExecutor);
@@ -352,50 +309,63 @@
}
private void registerMessageHandlers(ExecutorService executor) {
-
clusterCommunicator.addSubscriber(APPLY_BATCH_FLOWS, new OnStoreBatch(), executor);
clusterCommunicator.<FlowRuleBatchEvent>addSubscriber(
- REMOTE_APPLY_COMPLETED, serializer::decode, this::notifyDelegate, executor);
+ REMOTE_APPLY_COMPLETED, serializer::decode, this::notifyDelegate, executor);
clusterCommunicator.addSubscriber(
- GET_FLOW_ENTRY, serializer::decode, flowTable::getFlowEntry, serializer::encode, executor);
+ GET_FLOW_ENTRY, serializer::decode, flowTable::getFlowEntry, serializer::encode, executor);
clusterCommunicator.addSubscriber(
- GET_DEVICE_FLOW_ENTRIES, serializer::decode, flowTable::getFlowEntries, serializer::encode, executor);
+ GET_DEVICE_FLOW_ENTRIES, serializer::decode, flowTable::getFlowEntries, serializer::encode, executor);
clusterCommunicator.addSubscriber(
- REMOVE_FLOW_ENTRY, serializer::decode, this::removeFlowRuleInternal, serializer::encode, executor);
+ GET_DEVICE_FLOW_COUNT, serializer::decode, flowTable::getFlowRuleCount, serializer::encode, executor);
clusterCommunicator.addSubscriber(
- FLOW_TABLE_BACKUP, serializer::decode, flowTable::onBackup, serializer::encode, executor);
- clusterCommunicator.addSubscriber(
- FLOW_TABLE_ANTI_ENTROPY, serializer::decode, flowTable::onAntiEntropy, serializer::encode, executor);
+ REMOVE_FLOW_ENTRY, serializer::decode, this::removeFlowRuleInternal, serializer::encode, executor);
}
private void unregisterMessageHandlers() {
clusterCommunicator.removeSubscriber(REMOVE_FLOW_ENTRY);
clusterCommunicator.removeSubscriber(GET_DEVICE_FLOW_ENTRIES);
+ clusterCommunicator.removeSubscriber(GET_DEVICE_FLOW_COUNT);
clusterCommunicator.removeSubscriber(GET_FLOW_ENTRY);
clusterCommunicator.removeSubscriber(APPLY_BATCH_FLOWS);
clusterCommunicator.removeSubscriber(REMOTE_APPLY_COMPLETED);
clusterCommunicator.removeSubscriber(FLOW_TABLE_BACKUP);
- clusterCommunicator.removeSubscriber(FLOW_TABLE_ANTI_ENTROPY);
}
private void logConfig(String prefix) {
log.info("{} with msgHandlerPoolSize = {}; backupPeriod = {}, backupCount = {}",
- prefix, msgHandlerPoolSize, backupPeriod, backupCount);
+ prefix, msgHandlerPoolSize, backupPeriod, backupCount);
}
@Override
public int getFlowRuleCount() {
return Streams.stream(deviceService.getDevices()).parallel()
- .mapToInt(device -> getFlowRuleCount(device.id()))
- .sum();
+ .mapToInt(device -> getFlowRuleCount(device.id()))
+ .sum();
}
@Override
public int getFlowRuleCount(DeviceId deviceId) {
- return flowCounts.entrySet().stream()
- .filter(entry -> entry.getKey().deviceId().equals(deviceId))
- .mapToInt(entry -> entry.getValue())
- .sum();
+ NodeId master = mastershipService.getMasterFor(deviceId);
+ if (master == null) {
+ log.debug("Failed to getFlowRuleCount: No master for {}", deviceId);
+ return 0;
+ }
+
+ if (Objects.equals(local, master)) {
+ return flowTable.getFlowRuleCount(deviceId);
+ }
+
+ log.trace("Forwarding getFlowRuleCount to master {} for device {}", master, deviceId);
+ return Tools.futureGetOrElse(clusterCommunicator.sendAndReceive(
+ deviceId,
+ GET_DEVICE_FLOW_COUNT,
+ serializer::encode,
+ serializer::decode,
+ master),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ 0);
}
@Override
@@ -412,16 +382,16 @@
}
log.trace("Forwarding getFlowEntry to {}, which is the primary (master) for device {}",
- master, rule.deviceId());
+ master, rule.deviceId());
return Tools.futureGetOrElse(clusterCommunicator.sendAndReceive(rule,
- ECFlowRuleStoreMessageSubjects.GET_FLOW_ENTRY,
- serializer::encode,
- serializer::decode,
- master),
- FLOW_RULE_STORE_TIMEOUT_MILLIS,
- TimeUnit.MILLISECONDS,
- null);
+ ECFlowRuleStoreMessageSubjects.GET_FLOW_ENTRY,
+ serializer::encode,
+ serializer::decode,
+ master),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
}
@Override
@@ -438,31 +408,31 @@
}
log.trace("Forwarding getFlowEntries to {}, which is the primary (master) for device {}",
- master, deviceId);
+ master, deviceId);
return Tools.futureGetOrElse(clusterCommunicator.sendAndReceive(deviceId,
- ECFlowRuleStoreMessageSubjects.GET_DEVICE_FLOW_ENTRIES,
- serializer::encode,
- serializer::decode,
- master),
- FLOW_RULE_STORE_TIMEOUT_MILLIS,
- TimeUnit.MILLISECONDS,
- Collections.emptyList());
+ ECFlowRuleStoreMessageSubjects.GET_DEVICE_FLOW_ENTRIES,
+ serializer::encode,
+ serializer::decode,
+ master),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ Collections.emptyList());
}
@Override
public void storeFlowRule(FlowRule rule) {
storeBatch(new FlowRuleBatchOperation(
- Collections.singletonList(new FlowRuleBatchEntry(FlowRuleOperation.ADD, rule)),
- rule.deviceId(), idGenerator.getNewId()));
+ Collections.singletonList(new FlowRuleBatchEntry(FlowRuleOperation.ADD, rule)),
+ rule.deviceId(), idGenerator.getNewId()));
}
@Override
public void storeBatch(FlowRuleBatchOperation operation) {
if (operation.getOperations().isEmpty()) {
notifyDelegate(FlowRuleBatchEvent.completed(
- new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
- new CompletedBatchOperation(true, Collections.emptySet(), operation.deviceId())));
+ new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+ new CompletedBatchOperation(true, Collections.emptySet(), operation.deviceId())));
return;
}
@@ -472,11 +442,13 @@
if (master == null) {
log.warn("No master for {} ", deviceId);
- updateStoreInternal(operation);
-
+ Set<FlowRule> allFailures = operation.getOperations()
+ .stream()
+ .map(op -> op.target())
+ .collect(Collectors.toSet());
notifyDelegate(FlowRuleBatchEvent.completed(
- new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
- new CompletedBatchOperation(true, Collections.emptySet(), operation.deviceId())));
+ new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+ new CompletedBatchOperation(false, allFailures, deviceId)));
return;
}
@@ -486,26 +458,26 @@
}
log.trace("Forwarding storeBatch to {}, which is the primary (master) for device {}",
- master, deviceId);
+ master, deviceId);
clusterCommunicator.unicast(operation,
- APPLY_BATCH_FLOWS,
- serializer::encode,
- master)
- .whenComplete((result, error) -> {
- if (error != null) {
- log.warn("Failed to storeBatch: {} to {}", operation, master, error);
+ APPLY_BATCH_FLOWS,
+ serializer::encode,
+ master)
+ .whenComplete((result, error) -> {
+ if (error != null) {
+ log.warn("Failed to storeBatch: {} to {}", operation, master, error);
- Set<FlowRule> allFailures = operation.getOperations()
- .stream()
- .map(op -> op.target())
- .collect(Collectors.toSet());
+ Set<FlowRule> allFailures = operation.getOperations()
+ .stream()
+ .map(op -> op.target())
+ .collect(Collectors.toSet());
- notifyDelegate(FlowRuleBatchEvent.completed(
- new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
- new CompletedBatchOperation(false, allFailures, deviceId)));
- }
- });
+ notifyDelegate(FlowRuleBatchEvent.completed(
+ new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+ new CompletedBatchOperation(false, allFailures, deviceId)));
+ }
+ });
}
private void storeBatchInternal(FlowRuleBatchOperation operation) {
@@ -515,65 +487,63 @@
Set<FlowRuleBatchEntry> currentOps = updateStoreInternal(operation);
if (currentOps.isEmpty()) {
batchOperationComplete(FlowRuleBatchEvent.completed(
- new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
- new CompletedBatchOperation(true, Collections.emptySet(), did)));
+ new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+ new CompletedBatchOperation(true, Collections.emptySet(), did)));
return;
}
notifyDelegate(FlowRuleBatchEvent.requested(new
- FlowRuleBatchRequest(operation.id(),
- currentOps), operation.deviceId()));
+ FlowRuleBatchRequest(operation.id(),
+ currentOps), operation.deviceId()));
}
private Set<FlowRuleBatchEntry> updateStoreInternal(FlowRuleBatchOperation operation) {
return operation.getOperations().stream().map(
- op -> {
- StoredFlowEntry entry;
- switch (op.operator()) {
- case ADD:
- entry = new DefaultFlowEntry(op.target());
- flowTable.add(entry);
+ op -> {
+ StoredFlowEntry entry;
+ switch (op.operator()) {
+ case ADD:
+ entry = new DefaultFlowEntry(op.target());
+ flowTable.add(entry);
+ return op;
+ case MODIFY:
+ entry = new DefaultFlowEntry(op.target());
+ flowTable.update(entry);
+ return op;
+ case REMOVE:
+ return flowTable.update(op.target(), stored -> {
+ stored.setState(FlowEntryState.PENDING_REMOVE);
+ log.debug("Setting state of rule to pending remove: {}", stored);
return op;
- case MODIFY:
- entry = new DefaultFlowEntry(op.target());
- flowTable.update(entry);
- return op;
- case REMOVE:
- entry = flowTable.getFlowEntry(op.target());
- if (entry != null) {
- entry.setState(FlowEntryState.PENDING_REMOVE);
- flowTable.update(entry);
- log.debug("Setting state of rule to pending remove: {}", entry);
- return op;
- }
- break;
- default:
- log.warn("Unknown flow operation operator: {}", op.operator());
- }
- return null;
+ });
+ default:
+ log.warn("Unknown flow operation operator: {}", op.operator());
}
+ return null;
+ }
).filter(Objects::nonNull).collect(Collectors.toSet());
}
@Override
public void deleteFlowRule(FlowRule rule) {
storeBatch(
- new FlowRuleBatchOperation(
- Collections.singletonList(
- new FlowRuleBatchEntry(
- FlowRuleOperation.REMOVE,
- rule)), rule.deviceId(), idGenerator.getNewId()));
+ new FlowRuleBatchOperation(
+ Collections.singletonList(
+ new FlowRuleBatchEntry(
+ FlowRuleOperation.REMOVE,
+ rule)), rule.deviceId(), idGenerator.getNewId()));
}
@Override
public FlowRuleEvent pendingFlowRule(FlowEntry rule) {
if (mastershipService.isLocalMaster(rule.deviceId())) {
- StoredFlowEntry stored = flowTable.getFlowEntry(rule);
- if (stored != null &&
- stored.state() != FlowEntryState.PENDING_ADD) {
- stored.setState(FlowEntryState.PENDING_ADD);
- return new FlowRuleEvent(Type.RULE_UPDATED, rule);
- }
+ return flowTable.update(rule, stored -> {
+ if (stored.state() == FlowEntryState.PENDING_ADD) {
+ stored.setState(FlowEntryState.PENDING_ADD);
+ return new FlowRuleEvent(Type.RULE_UPDATED, rule);
+ }
+ return null;
+ });
}
return null;
}
@@ -586,14 +556,12 @@
}
log.warn("Tried to update FlowRule {} state,"
- + " while the Node was not the master.", rule);
+ + " while the Node was not the master.", rule);
return null;
}
private FlowRuleEvent addOrUpdateFlowRuleInternal(FlowEntry rule) {
- // check if this new rule is an update to an existing entry
- StoredFlowEntry stored = flowTable.getFlowEntry(rule);
- if (stored != null) {
+ FlowRuleEvent event = flowTable.update(rule, stored -> {
stored.setBytes(rule.bytes());
stored.setLife(rule.life(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
stored.setLiveType(rule.liveType());
@@ -601,11 +569,12 @@
stored.setLastSeen();
if (stored.state() == FlowEntryState.PENDING_ADD) {
stored.setState(FlowEntryState.ADDED);
- // Update the flow table to ensure the changes are replicated
- flowTable.update(stored);
return new FlowRuleEvent(Type.RULE_ADDED, rule);
}
return new FlowRuleEvent(Type.RULE_UPDATED, rule);
+ });
+ if (event != null) {
+ return event;
}
// TODO: Confirm if this behavior is correct. See SimpleFlowRuleStore
@@ -631,14 +600,17 @@
}
log.trace("Forwarding removeFlowRule to {}, which is the master for device {}",
- master, deviceId);
+ master, deviceId);
- return Futures.getUnchecked(clusterCommunicator.sendAndReceive(
- rule,
- REMOVE_FLOW_ENTRY,
- serializer::encode,
- serializer::decode,
- master));
+ return Tools.futureGetOrElse(clusterCommunicator.sendAndReceive(
+ rule,
+ REMOVE_FLOW_ENTRY,
+ serializer::encode,
+ serializer::decode,
+ master),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
}
private FlowRuleEvent removeFlowRuleInternal(FlowEntry rule) {
@@ -699,675 +671,173 @@
}
}
- /**
- * Represents a backup to a of a distinct bucket to a distinct node.
- */
- private class BackupOperation {
- private final NodeId nodeId;
- private final BucketId bucketId;
-
- BackupOperation(NodeId nodeId, BucketId bucketId) {
- this.nodeId = nodeId;
- this.bucketId = bucketId;
- }
-
- NodeId nodeId() {
- return nodeId;
- }
-
- BucketId bucketId() {
- return bucketId;
- }
+ private class InternalFlowTable implements DeviceListener {
+ private final Map<DeviceId, DeviceFlowTable> flowTables = Maps.newConcurrentMap();
@Override
- public int hashCode() {
- return Objects.hash(nodeId, bucketId);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other != null && other instanceof BackupOperation) {
- BackupOperation that = (BackupOperation) other;
- return this.nodeId.equals(that.nodeId)
- && this.bucketId.equals(that.bucketId);
- }
- return false;
- }
- }
-
- /**
- * Represents a distinct device flow bucket.
- */
- private class BucketId {
- private final DeviceId deviceId;
- private final int bucket;
-
- BucketId(DeviceId deviceId, int bucket) {
- this.deviceId = deviceId;
- this.bucket = bucket;
- }
-
- DeviceId deviceId() {
- return deviceId;
- }
-
- int bucket() {
- return bucket;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(deviceId, bucket);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other != null && other instanceof BucketId) {
- BucketId that = (BucketId) other;
- return this.deviceId.equals(that.deviceId)
- && this.bucket == that.bucket;
- }
- return false;
- }
- }
-
- /**
- * Container for flows in a specific bucket.
- */
- private class FlowBucket {
- private final BucketId bucketId;
- private final Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> table;
- private final long timestamp;
-
- BucketId bucketId() {
- return bucketId;
- }
-
- Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> table() {
- return table;
- }
-
- long timestamp() {
- return timestamp;
- }
-
- FlowBucket(BucketId bucketId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> table, long timestamp) {
- this.bucketId = bucketId;
- this.table = table;
- this.timestamp = timestamp;
- }
- }
-
- /**
- * Device digest.
- */
- private class DeviceDigest {
- private final DeviceId deviceId;
- private final Set<FlowBucketDigest> digests;
-
- DeviceDigest(DeviceId deviceId, Set<FlowBucketDigest> digests) {
- this.deviceId = deviceId;
- this.digests = digests;
- }
-
- DeviceId deviceId() {
- return deviceId;
- }
-
- Set<FlowBucketDigest> digests() {
- return digests;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(deviceId, digests);
- }
-
- @Override
- public boolean equals(Object object) {
- return object instanceof DeviceDigest
- && ((DeviceDigest) object).deviceId.equals(deviceId);
- }
- }
-
- /**
- * Flow bucket digest.
- */
- private class FlowBucketDigest {
- private final BucketId bucketId;
- private final long timestamp;
-
- FlowBucketDigest(BucketId bucketId, long timestamp) {
- this.bucketId = bucketId;
- this.timestamp = timestamp;
- }
-
- BucketId bucketId() {
- return bucketId;
- }
-
- long timestamp() {
- return timestamp;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(bucketId);
- }
-
- @Override
- public boolean equals(Object object) {
- return object instanceof FlowBucketDigest
- && ((FlowBucketDigest) object).bucketId.equals(bucketId);
- }
- }
-
- private class InternalFlowTable implements ReplicaInfoEventListener {
-
- //TODO replace the Map<V,V> with ExtendedSet
- private final Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
- flowEntries = Maps.newConcurrentMap();
-
- private final Map<BackupOperation, Long> lastBackupTimes = Maps.newConcurrentMap();
- private final Map<BucketId, Long> lastUpdateTimes = Maps.newConcurrentMap();
- private final Set<BackupOperation> inFlightUpdates = Sets.newConcurrentHashSet();
-
- private final AtomicLong currentTimestamp = new AtomicLong();
-
- @Override
- public void event(ReplicaInfoEvent event) {
- eventHandler.execute(() -> handleEvent(event));
- }
-
- /**
- * Handles a replica change event.
- *
- * @param event the replica change event to handle
- */
- private void handleEvent(ReplicaInfoEvent event) {
- DeviceId deviceId = event.subject();
-
- // If the local node is not the master, return.
- if (!isMasterNode(deviceId)) {
- // If the local node is neither the master or a backup, remove flow tables for the device.
- if (!isBackupNode(deviceId)) {
- purgeFlowRule(deviceId);
- }
- return;
- }
- backupSenderExecutor.execute(this::runAntiEntropy);
- }
-
- /**
- * Returns the set of devices in the flow table.
- *
- * @return the set of devices in the flow table
- */
- private Set<DeviceId> getDevices() {
- return flowEntries.keySet();
- }
-
- /**
- * Returns the digests for all buckets in the flow table for the given device.
- *
- * @param deviceId the device for which to return digests
- * @return the set of digests for all buckets for the given device
- */
- private Set<FlowBucketDigest> getDigests(DeviceId deviceId) {
- return IntStream.range(0, NUM_BUCKETS)
- .mapToObj(bucket -> {
- BucketId bucketId = new BucketId(deviceId, bucket);
- long timestamp = lastUpdateTimes.getOrDefault(bucketId, 0L);
- return new FlowBucketDigest(bucketId, timestamp);
- }).collect(Collectors.toSet());
- }
-
- /**
- * Returns the flow table for specified device.
- *
- * @param deviceId identifier of the device
- * @return Map representing Flow Table of given device.
- */
- private Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> getFlowTable(DeviceId deviceId) {
- // Use an external get/null check to avoid locks.
- // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8161372
- if (persistenceEnabled) {
- Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> flowTable = flowEntries.get(deviceId);
- return flowTable != null ? flowTable
- : flowEntries.computeIfAbsent(deviceId, id ->
- persistenceService.<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>persistentMapBuilder()
- .withName("FlowTable:" + deviceId.toString())
- .withSerializer(serializer)
- .build());
- } else {
- Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> flowTable = flowEntries.get(deviceId);
- return flowTable != null ? flowTable
- : flowEntries.computeIfAbsent(deviceId, id -> Maps.newConcurrentMap());
+ public void event(DeviceEvent event) {
+ if (event.type() == DeviceEvent.Type.DEVICE_ADDED) {
+ addDevice(event.subject().id());
}
}
- private FlowBucket getFlowBucket(BucketId bucketId) {
- long timestamp = lastUpdateTimes.getOrDefault(bucketId, 0L);
- return new FlowBucket(bucketId, getFlowTable(bucketId.deviceId())
- .entrySet()
- .stream()
- .filter(entry -> isInBucket(entry.getKey(), bucketId.bucket()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
- timestamp);
+ /**
+ * Adds the given device to the flow table.
+ *
+ * @param deviceId the device to add to the table
+ */
+ public void addDevice(DeviceId deviceId) {
+ flowTables.computeIfAbsent(deviceId, id -> new DeviceFlowTable(
+ id,
+ clusterService,
+ clusterCommunicator,
+ new InternalLifecycleManager(id),
+ backupSenderExecutor,
+ backupPeriod,
+ antiEntropyPeriod));
}
- private Map<StoredFlowEntry, StoredFlowEntry> getFlowEntriesInternal(DeviceId deviceId, FlowId flowId) {
- // Use an external get/null check to avoid locks.
- // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8161372
- Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> flowTable = getFlowTable(deviceId);
- Map<StoredFlowEntry, StoredFlowEntry> flowEntries = flowTable.get(flowId);
- return flowEntries != null ? flowEntries : flowTable.computeIfAbsent(flowId, id -> Maps.newConcurrentMap());
+ /**
+ * Sets the flow table backup period.
+ *
+ * @param backupPeriod the flow table backup period
+ */
+ void setBackupPeriod(int backupPeriod) {
+ flowTables.values().forEach(flowTable -> flowTable.setBackupPeriod(backupPeriod));
}
- private StoredFlowEntry getFlowEntryInternal(FlowRule rule) {
- return getFlowEntriesInternal(rule.deviceId(), rule.id()).get(rule);
+ /**
+ * Sets the flow table anti-entropy period.
+ *
+ * @param antiEntropyPeriod the flow table anti-entropy period
+ */
+ void setAntiEntropyPeriod(int antiEntropyPeriod) {
+ flowTables.values().forEach(flowTable -> flowTable.setAntiEntropyPeriod(antiEntropyPeriod));
}
- private Set<FlowEntry> getFlowEntriesInternal(DeviceId deviceId) {
- return getFlowTable(deviceId).values().stream()
- .flatMap(m -> m.values().stream())
- .collect(Collectors.toSet());
+ /**
+ * Returns the flow table for a specific device.
+ *
+ * @param deviceId the device identifier
+ * @return the flow table for the given device
+ */
+ private DeviceFlowTable getFlowTable(DeviceId deviceId) {
+ DeviceFlowTable flowTable = flowTables.get(deviceId);
+ return flowTable != null ? flowTable : flowTables.computeIfAbsent(deviceId, id -> new DeviceFlowTable(
+ deviceId,
+ clusterService,
+ clusterCommunicator,
+ new InternalLifecycleManager(deviceId),
+ backupSenderExecutor,
+ backupPeriod,
+ antiEntropyPeriod));
}
+ /**
+ * Returns the flow rule count for the given device.
+ *
+ * @param deviceId the device for which to return the flow rule count
+ * @return the flow rule count for the given device
+ */
+ public int getFlowRuleCount(DeviceId deviceId) {
+ return getFlowTable(deviceId).count();
+ }
+
+ /**
+ * Returns the flow entry for the given rule.
+ *
+ * @param rule the rule for which to return the flow entry
+ * @return the flow entry for the given rule
+ */
public StoredFlowEntry getFlowEntry(FlowRule rule) {
- return getFlowEntryInternal(rule);
+ return getFlowTable(rule.deviceId()).getFlowEntry(rule);
}
+ /**
+ * Returns the set of flow entries for the given device.
+ *
+ * @param deviceId the device for which to lookup flow entries
+ * @return the set of flow entries for the given device
+ */
public Set<FlowEntry> getFlowEntries(DeviceId deviceId) {
- return getFlowEntriesInternal(deviceId);
+ return getFlowTable(deviceId).getFlowEntries();
}
- private boolean isInBucket(FlowId flowId, int bucket) {
- return bucket(flowId) == bucket;
- }
-
- private int bucket(FlowId flowId) {
- return (int) (flowId.id() % NUM_BUCKETS);
- }
-
- private void recordUpdate(BucketId bucketId) {
- recordUpdate(bucketId, currentTimestamp.accumulateAndGet(System.currentTimeMillis(), Math::max));
- }
-
- private void recordUpdate(BucketId bucketId, long timestamp) {
- lastUpdateTimes.put(bucketId, timestamp);
- }
-
+ /**
+ * Adds the given flow rule.
+ *
+ * @param rule the rule to add
+ */
public void add(FlowEntry rule) {
- getFlowEntriesInternal(rule.deviceId(), rule.id())
- .compute((StoredFlowEntry) rule, (k, stored) -> {
- return (StoredFlowEntry) rule;
- });
- recordUpdate(new BucketId(rule.deviceId(), bucket(rule.id())));
+ Tools.futureGetOrElse(
+ getFlowTable(rule.deviceId()).add(rule),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
}
+ /**
+ * Updates the given flow rule.
+ *
+ * @param rule the rule to update
+ */
public void update(FlowEntry rule) {
- getFlowEntriesInternal(rule.deviceId(), rule.id())
- .computeIfPresent((StoredFlowEntry) rule, (k, stored) -> {
- if (rule instanceof DefaultFlowEntry) {
- DefaultFlowEntry updated = (DefaultFlowEntry) rule;
- if (stored instanceof DefaultFlowEntry) {
- DefaultFlowEntry storedEntry = (DefaultFlowEntry) stored;
- if (updated.created() >= storedEntry.created()) {
- recordUpdate(new BucketId(rule.deviceId(), bucket(rule.id())));
- return updated;
- } else {
- log.debug("Trying to update more recent flow entry {} (stored: {})", updated, stored);
- return stored;
- }
- }
- }
- return stored;
- });
+ Tools.futureGetOrElse(
+ getFlowTable(rule.deviceId()).update(rule),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
}
+ /**
+ * Applies the given update function to the rule.
+ *
+ * @param function the update function to apply
+ * @return a future to be completed with the update event or {@code null} if the rule was not updated
+ */
+ public <T> T update(FlowRule rule, Function<StoredFlowEntry, T> function) {
+ return Tools.futureGetOrElse(
+ getFlowTable(rule.deviceId()).update(rule, function),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
+ }
+
+ /**
+ * Removes the given flow rule.
+ *
+ * @param rule the rule to remove
+ */
public FlowEntry remove(FlowEntry rule) {
- final AtomicReference<FlowEntry> removedRule = new AtomicReference<>();
- final Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> flowTable = getFlowTable(rule.deviceId());
- flowTable.computeIfPresent(rule.id(), (flowId, flowEntries) -> {
- flowEntries.computeIfPresent((StoredFlowEntry) rule, (k, stored) -> {
- if (rule instanceof DefaultFlowEntry) {
- DefaultFlowEntry toRemove = (DefaultFlowEntry) rule;
- if (stored instanceof DefaultFlowEntry) {
- DefaultFlowEntry storedEntry = (DefaultFlowEntry) stored;
- if (toRemove.created() < storedEntry.created()) {
- log.debug("Trying to remove more recent flow entry {} (stored: {})", toRemove, stored);
- // the key is not updated, removedRule remains null
- return stored;
- }
- }
- }
- removedRule.set(stored);
- return null;
- });
- return flowEntries.isEmpty() ? null : flowEntries;
- });
-
- if (removedRule.get() != null) {
- recordUpdate(new BucketId(rule.deviceId(), bucket(rule.id())));
- return removedRule.get();
- } else {
- return null;
- }
+ return Tools.futureGetOrElse(
+ getFlowTable(rule.deviceId()).remove(rule),
+ FLOW_RULE_STORE_TIMEOUT_MILLIS,
+ TimeUnit.MILLISECONDS,
+ null);
}
+ /**
+ * Purges flow rules for the given device.
+ *
+ * @param deviceId the device for which to purge flow rules
+ */
public void purgeFlowRule(DeviceId deviceId) {
- flowEntries.remove(deviceId);
+ DeviceFlowTable flowTable = flowTables.remove(deviceId);
+ if (flowTable != null) {
+ flowTable.close();
+ }
}
+ /**
+ * Purges all flow rules from the table.
+ */
public void purgeFlowRules() {
- flowEntries.clear();
- }
-
- /**
- * Returns a boolean indicating whether the local node is the current master for the given device.
- *
- * @param deviceId the device for which to indicate whether the local node is the current master
- * @return indicates whether the local node is the current master for the given device
- */
- private boolean isMasterNode(DeviceId deviceId) {
- NodeId master = replicaInfoManager.getReplicaInfoFor(deviceId).master().orElse(null);
- return Objects.equals(master, clusterService.getLocalNode().id());
- }
-
- /**
- * Returns a boolean indicating whether the local node is a backup for the given device.
- *
- * @param deviceId the device for which to indicate whether the local node is a backup
- * @return indicates whether the local node is a backup for the given device
- */
- private boolean isBackupNode(DeviceId deviceId) {
- List<NodeId> backupNodes = replicaInfoManager.getReplicaInfoFor(deviceId).backups();
- int index = backupNodes.indexOf(local);
- return index != -1 && index < backupCount;
- }
-
- /**
- * Backs up all devices to all backup nodes.
- */
- private void backup() {
- for (DeviceId deviceId : getDevices()) {
- backup(deviceId);
+ Iterator<DeviceFlowTable> iterator = flowTables.values().iterator();
+ while (iterator.hasNext()) {
+ iterator.next().close();
+ iterator.remove();
}
}
-
- /**
- * Backs up all buckets in the given device to the given node.
- *
- * @param deviceId the device to back up
- */
- private void backup(DeviceId deviceId) {
- if (!isMasterNode(deviceId)) {
- return;
- }
-
- // Get a list of backup nodes for the device.
- List<NodeId> backupNodes = replicaInfoManager.getReplicaInfoFor(deviceId).backups();
- int availableBackupCount = Math.min(backupCount, backupNodes.size());
-
- // If the list of backup nodes is empty, update the flow count.
- if (availableBackupCount == 0) {
- updateDeviceFlowCounts(deviceId);
- } else {
- // Otherwise, iterate through backup nodes and backup the device.
- for (int index = 0; index < availableBackupCount; index++) {
- NodeId backupNode = backupNodes.get(index);
- try {
- backup(deviceId, backupNode);
- } catch (Exception e) {
- log.error("Backup of " + deviceId + " to " + backupNode + " failed", e);
- }
- }
- }
- }
-
- /**
- * Backs up all buckets for the given device to the given node.
- *
- * @param deviceId the device to back up
- * @param nodeId the node to which to back up the device
- */
- private void backup(DeviceId deviceId, NodeId nodeId) {
- final long timestamp = System.currentTimeMillis();
- for (int bucket = 0; bucket < NUM_BUCKETS; bucket++) {
- BucketId bucketId = new BucketId(deviceId, bucket);
- BackupOperation operation = new BackupOperation(nodeId, bucketId);
- if (startBackup(operation)) {
- backup(operation).whenCompleteAsync((succeeded, error) -> {
- if (error == null && succeeded) {
- succeedBackup(operation, timestamp);
- } else {
- failBackup(operation);
- }
- backup(deviceId, nodeId);
- }, backupSenderExecutor);
- }
- }
- }
-
- /**
- * Returns a boolean indicating whether the given {@link BackupOperation} can be started.
- * <p>
- * The backup can be started if no backup for the same device/bucket/node is already in progress and changes
- * are pending replication for the backup operation.
- *
- * @param operation the operation to start
- * @return indicates whether the given backup operation should be started
- */
- private boolean startBackup(BackupOperation operation) {
- long lastBackupTime = lastBackupTimes.getOrDefault(operation, 0L);
- long lastUpdateTime = lastUpdateTimes.getOrDefault(operation.bucketId(), 0L);
- return lastUpdateTime > 0 && lastBackupTime <= lastUpdateTime && inFlightUpdates.add(operation);
- }
-
- /**
- * Fails the given backup operation.
- *
- * @param operation the backup operation to fail
- */
- private void failBackup(BackupOperation operation) {
- inFlightUpdates.remove(operation);
- }
-
- /**
- * Succeeds the given backup operation.
- * <p>
- * The last backup time for the operation will be updated and the operation will be removed from
- * in-flight updates.
- *
- * @param operation the operation to succeed
- * @param timestamp the timestamp at which the operation was <em>started</em>
- */
- private void succeedBackup(BackupOperation operation, long timestamp) {
- lastBackupTimes.put(operation, timestamp);
- inFlightUpdates.remove(operation);
- }
-
- /**
- * Performs the given backup operation.
- *
- * @param operation the operation to perform
- * @return a future to be completed with a boolean indicating whether the backup operation was successful
- */
- private CompletableFuture<Boolean> backup(BackupOperation operation) {
- log.debug("Sending flowEntries in bucket {} for device {} to {} for backup.",
- operation.bucketId().bucket(), operation.bucketId().deviceId(), operation.nodeId());
- FlowBucket flowBucket = getFlowBucket(operation.bucketId());
-
- CompletableFuture<Boolean> future = new CompletableFuture<>();
- clusterCommunicator.<FlowBucket, Set<FlowId>>sendAndReceive(
- flowBucket,
- FLOW_TABLE_BACKUP,
- serializer::encode,
- serializer::decode,
- operation.nodeId())
- .whenComplete((backedupFlows, error) -> {
- Set<FlowId> flowsNotBackedUp = error != null ?
- flowBucket.table().keySet() :
- Sets.difference(flowBucket.table().keySet(), backedupFlows);
- if (flowsNotBackedUp.size() > 0) {
- log.warn("Failed to backup flows: {}. Reason: {}, Node: {}",
- flowsNotBackedUp, error != null ? error.getMessage() : "none", operation.nodeId());
- }
- future.complete(backedupFlows != null);
- });
-
- updateFlowCounts(flowBucket);
- return future;
- }
-
- /**
- * Handles a flow bucket backup from a remote peer.
- *
- * @param flowBucket the flow bucket to back up
- * @return the set of flows that could not be backed up
- */
- private Set<FlowId> onBackup(FlowBucket flowBucket) {
- log.debug("Received flowEntries for {} bucket {} to backup",
- flowBucket.bucketId().deviceId(), flowBucket.bucketId);
- Set<FlowId> backedupFlows = Sets.newHashSet();
- try {
- // Only process those devices are that not managed by the local node.
- NodeId master = replicaInfoManager.getReplicaInfoFor(flowBucket.bucketId().deviceId())
- .master()
- .orElse(null);
- if (!Objects.equals(local, master)) {
- Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> backupFlowTable =
- getFlowTable(flowBucket.bucketId().deviceId());
- backupFlowTable.putAll(flowBucket.table());
- backupFlowTable.entrySet()
- .removeIf(entry -> isInBucket(entry.getKey(), flowBucket.bucketId().bucket())
- && !flowBucket.table().containsKey(entry.getKey()));
- backedupFlows.addAll(flowBucket.table().keySet());
- recordUpdate(flowBucket.bucketId(), flowBucket.timestamp());
- }
- } catch (Exception e) {
- log.warn("Failure processing backup request", e);
- }
- return backedupFlows;
- }
-
- /**
- * Runs the anti-entropy protocol.
- */
- private void runAntiEntropy() {
- for (DeviceId deviceId : getDevices()) {
- runAntiEntropy(deviceId);
- }
- }
-
- /**
- * Runs the anti-entropy protocol for the given device.
- *
- * @param deviceId the device for which to run the anti-entropy protocol
- */
- private void runAntiEntropy(DeviceId deviceId) {
- if (!isMasterNode(deviceId)) {
- return;
- }
-
- // Get the set of digests for the node.
- Set<FlowBucketDigest> digests = getDigests(deviceId);
-
- // Get a list of backup nodes for the device and compute the real backup count.
- List<NodeId> backupNodes = replicaInfoManager.getReplicaInfoFor(deviceId).backups();
- int availableBackupCount = Math.min(backupCount, backupNodes.size());
-
- // Iterate through backup nodes and run the anti-entropy protocol.
- for (int index = 0; index < availableBackupCount; index++) {
- NodeId backupNode = backupNodes.get(index);
- try {
- runAntiEntropy(deviceId, backupNode, digests);
- } catch (Exception e) {
- log.error("Anti-entropy for " + deviceId + " to " + backupNode + " failed", e);
- }
- }
- }
-
- /**
- * Sends an anti-entropy advertisement to the given node.
- *
- * @param deviceId the device ID for which to send the advertisement
- * @param nodeId the node to which to send the advertisement
- * @param digests the digests to send to the given node
- */
- private void runAntiEntropy(DeviceId deviceId, NodeId nodeId, Set<FlowBucketDigest> digests) {
- log.trace("Sending anti-entropy advertisement for device {} to {}", deviceId, nodeId);
- clusterCommunicator.<Set<FlowBucketDigest>, Set<BucketId>>sendAndReceive(
- digests,
- FLOW_TABLE_ANTI_ENTROPY,
- serializer::encode,
- serializer::decode,
- nodeId)
- .whenComplete((missingBuckets, error) -> {
- if (error == null) {
- log.debug("Detected {} missing buckets on node {} for device {}",
- missingBuckets.size(), nodeId, deviceId);
- } else {
- log.trace("Anti-entropy advertisement for device {} to {} failed", deviceId, nodeId, error);
- }
- });
- }
-
- /**
- * Handles a device anti-entropy request from a remote peer.
- *
- * @param digest the device digest
- * @return the set of flow buckets to update
- */
- private Set<BucketId> onAntiEntropy(DeviceDigest digest) {
- // If the local node is the master, reject the anti-entropy request.
- // TODO: We really should be using mastership terms in anti-entropy requests to determine whether
- // this node is a newer master, but that would only reduce the time it takes to resolve missing flows
- // as a later anti-entropy request will still succeed once this node recognizes it's no longer the master.
- NodeId master = replicaInfoManager.getReplicaInfoFor(digest.deviceId())
- .master()
- .orElse(null);
- if (Objects.equals(master, local)) {
- return ImmutableSet.of();
- }
-
- // Compute a set of missing BucketIds based on digest times and send them back to the master.
- Set<BucketId> missingBuckets = new HashSet<>();
- for (FlowBucketDigest flowBucketDigest : digest.digests()) {
- long lastUpdated = lastUpdateTimes.getOrDefault(flowBucketDigest.bucketId(), 0L);
- if (lastUpdated < flowBucketDigest.timestamp()) {
- missingBuckets.add(flowBucketDigest.bucketId());
- }
- }
- return missingBuckets;
- }
-
- /**
- * Updates all flow counts for the given device.
- *
- * @param deviceId the device for which to update flow counts
- */
- private void updateDeviceFlowCounts(DeviceId deviceId) {
- for (int bucket = 0; bucket < NUM_BUCKETS; bucket++) {
- BucketId bucketId = new BucketId(deviceId, bucket);
- FlowBucket flowBucket = getFlowBucket(bucketId);
- updateFlowCounts(flowBucket);
- }
- }
-
- /**
- * Updates the eventually consistent flow count for the given bucket.
- *
- * @param flowBucket the flow bucket for which to update flow counts
- */
- private void updateFlowCounts(FlowBucket flowBucket) {
- int flowCount = flowBucket.table().entrySet()
- .stream()
- .mapToInt(e -> e.getValue().values().size())
- .sum();
- flowCounts.put(flowBucket.bucketId(), flowCount);
- }
}
@Override
@@ -1395,16 +865,114 @@
@Override
public long getActiveFlowRuleCount(DeviceId deviceId) {
return Streams.stream(getTableStatistics(deviceId))
- .mapToLong(TableStatisticsEntry::activeFlowEntries)
- .sum();
+ .mapToLong(TableStatisticsEntry::activeFlowEntries)
+ .sum();
}
private class InternalTableStatsListener
implements EventuallyConsistentMapListener<DeviceId, List<TableStatisticsEntry>> {
@Override
public void event(EventuallyConsistentMapEvent<DeviceId,
- List<TableStatisticsEntry>> event) {
+ List<TableStatisticsEntry>> event) {
//TODO: Generate an event to listeners (do we need?)
}
}
+
+ /**
+ * Device lifecycle manager implementation.
+ */
+ private final class InternalLifecycleManager
+ extends AbstractListenerManager<LifecycleEvent, LifecycleEventListener>
+ implements LifecycleManager, ReplicaInfoEventListener, MapEventListener<DeviceId, Long> {
+
+ private final DeviceId deviceId;
+
+ private volatile DeviceReplicaInfo replicaInfo;
+
+ InternalLifecycleManager(DeviceId deviceId) {
+ this.deviceId = deviceId;
+ replicaInfoManager.addListener(this);
+ mastershipTermLifecycles.addListener(this);
+ replicaInfo = toDeviceReplicaInfo(replicaInfoManager.getReplicaInfoFor(deviceId));
+ }
+
+ @Override
+ public DeviceReplicaInfo getReplicaInfo() {
+ return replicaInfo;
+ }
+
+ @Override
+ public void activate(long term) {
+ final ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(deviceId);
+ if (replicaInfo != null && replicaInfo.term() == term) {
+ mastershipTermLifecycles.put(deviceId, term);
+ }
+ }
+
+ @Override
+ public void event(ReplicaInfoEvent event) {
+ if (event.subject().equals(deviceId) && event.type() == ReplicaInfoEvent.Type.MASTER_CHANGED) {
+ onReplicaInfoChange(event.replicaInfo());
+ }
+ }
+
+ @Override
+ public void event(MapEvent<DeviceId, Long> event) {
+ if (event.key().equals(deviceId) && event.newValue() != null) {
+ onActivate(event.newValue().value());
+ }
+ }
+
+ /**
+ * Handles a term activation event.
+ *
+ * @param term the term that was activated
+ */
+ private void onActivate(long term) {
+ final ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(deviceId);
+ if (replicaInfo != null && replicaInfo.term() == term) {
+ NodeId master = replicaInfo.master().orElse(null);
+ List<NodeId> backups = replicaInfo.backups()
+ .subList(0, Math.min(replicaInfo.backups().size(), backupCount));
+ listenerRegistry.process(new LifecycleEvent(
+ LifecycleEvent.Type.TERM_ACTIVE,
+ new DeviceReplicaInfo(term, master, backups)));
+ }
+ }
+
+ /**
+ * Handles a replica info change event.
+ *
+ * @param replicaInfo the updated replica info
+ */
+ private synchronized void onReplicaInfoChange(ReplicaInfo replicaInfo) {
+ DeviceReplicaInfo oldReplicaInfo = this.replicaInfo;
+ this.replicaInfo = toDeviceReplicaInfo(replicaInfo);
+ if (oldReplicaInfo == null || oldReplicaInfo.term() < replicaInfo.term()) {
+ if (oldReplicaInfo != null) {
+ listenerRegistry.process(new LifecycleEvent(LifecycleEvent.Type.TERM_END, oldReplicaInfo));
+ }
+ listenerRegistry.process(new LifecycleEvent(LifecycleEvent.Type.TERM_START, this.replicaInfo));
+ }
+ }
+
+ /**
+ * Converts the given replica info into a {@link DeviceReplicaInfo} instance.
+ *
+ * @param replicaInfo the replica info to convert
+ * @return the converted replica info
+ */
+ private DeviceReplicaInfo toDeviceReplicaInfo(ReplicaInfo replicaInfo) {
+ NodeId master = replicaInfo.master().orElse(null);
+ List<NodeId> backups = replicaInfo.backups()
+ .subList(0, Math.min(replicaInfo.backups().size(), backupCount));
+ return new DeviceReplicaInfo(replicaInfo.term(), master, backups);
+ }
+
+ @Override
+ public void close() {
+ replicaInfoManager.removeListener(this);
+ mastershipTermLifecycles.removeListener(this);
+ }
+ }
}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStoreMessageSubjects.java
index 254f5df..c57e9eb 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStoreMessageSubjects.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStoreMessageSubjects.java
@@ -32,6 +32,9 @@
public static final MessageSubject GET_DEVICE_FLOW_ENTRIES
= new MessageSubject("peer-forward-get-device-flow-entries");
+ public static final MessageSubject GET_DEVICE_FLOW_COUNT
+ = new MessageSubject("peer-forward-get-flow-count");
+
public static final MessageSubject REMOVE_FLOW_ENTRY
= new MessageSubject("peer-forward-remove-flow-entry");
@@ -40,7 +43,4 @@
public static final MessageSubject FLOW_TABLE_BACKUP
= new MessageSubject("peer-flow-table-backup");
-
- public static final MessageSubject FLOW_TABLE_ANTI_ENTROPY
- = new MessageSubject("peer-flow-table-anti-entropy");
}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucket.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucket.java
new file mode 100644
index 0000000..2205b0b
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucket.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import com.google.common.collect.Maps;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.StoredFlowEntry;
+import org.onosproject.store.LogicalTimestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Container for a bucket of flows assigned to a specific device.
+ * <p>
+ * The bucket is mutable. When changes are made to the bucket, the term and timestamp in which the change
+ * occurred is recorded for ordering changes.
+ */
+public class FlowBucket {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FlowBucket.class);
+ private final BucketId bucketId;
+ private volatile long term;
+ private volatile LogicalTimestamp timestamp = new LogicalTimestamp(0);
+ private final Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> flowBucket = Maps.newConcurrentMap();
+
+ FlowBucket(BucketId bucketId) {
+ this.bucketId = bucketId;
+ }
+
+ /**
+ * Returns the flow bucket identifier.
+ *
+ * @return the flow bucket identifier
+ */
+ public BucketId bucketId() {
+ return bucketId;
+ }
+
+ /**
+ * Returns the flow bucket term.
+ *
+ * @return the flow bucket term
+ */
+ public long term() {
+ return term;
+ }
+
+ /**
+ * Returns the flow bucket timestamp.
+ *
+ * @return the flow bucket timestamp
+ */
+ public LogicalTimestamp timestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Returns the digest for the bucket.
+ *
+ * @return the digest for the bucket
+ */
+ public FlowBucketDigest getDigest() {
+ return new FlowBucketDigest(bucketId().bucket(), term(), timestamp());
+ }
+
+ /**
+ * Returns the flow entries in the bucket.
+ *
+ * @return the flow entries in the bucket
+ */
+ public Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> getFlowBucket() {
+ return flowBucket;
+ }
+
+ /**
+ * Returns the flow entries for the given flow.
+ *
+ * @param flowId the flow identifier
+ * @return the flows for the given flow ID
+ */
+ public Map<StoredFlowEntry, StoredFlowEntry> getFlowEntries(FlowId flowId) {
+ Map<StoredFlowEntry, StoredFlowEntry> flowEntries = flowBucket.get(flowId);
+ return flowEntries != null ? flowEntries : flowBucket.computeIfAbsent(flowId, id -> Maps.newConcurrentMap());
+ }
+
+ /**
+ * Counts the flows in the bucket.
+ *
+ * @return the number of flows in the bucket
+ */
+ public int count() {
+ return flowBucket.values()
+ .stream()
+ .mapToInt(entry -> entry.values().size())
+ .sum();
+ }
+
+ /**
+ * Records an update to the bucket.
+ */
+ private void recordUpdate(long term, LogicalTimestamp timestamp) {
+ this.term = term;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Adds the given flow rule to the bucket.
+ *
+ * @param rule the rule to add
+ * @param term the term in which the change occurred
+ * @param clock the logical clock
+ */
+ public void add(FlowEntry rule, long term, LogicalClock clock) {
+ Map<StoredFlowEntry, StoredFlowEntry> flowEntries = flowBucket.get(rule.id());
+ if (flowEntries == null) {
+ flowEntries = flowBucket.computeIfAbsent(rule.id(), id -> Maps.newConcurrentMap());
+ }
+ flowEntries.put((StoredFlowEntry) rule, (StoredFlowEntry) rule);
+ recordUpdate(term, clock.getTimestamp());
+ }
+
+ /**
+ * Updates the given flow rule in the bucket.
+ *
+ * @param rule the rule to update
+ * @param term the term in which the change occurred
+ * @param clock the logical clock
+ */
+ public void update(FlowEntry rule, long term, LogicalClock clock) {
+ Map<StoredFlowEntry, StoredFlowEntry> flowEntries = flowBucket.get(rule.id());
+ if (flowEntries == null) {
+ flowEntries = flowBucket.computeIfAbsent(rule.id(), id -> Maps.newConcurrentMap());
+ }
+ flowEntries.computeIfPresent((StoredFlowEntry) rule, (k, stored) -> {
+ if (rule instanceof DefaultFlowEntry) {
+ DefaultFlowEntry updated = (DefaultFlowEntry) rule;
+ if (stored instanceof DefaultFlowEntry) {
+ DefaultFlowEntry storedEntry = (DefaultFlowEntry) stored;
+ if (updated.created() >= storedEntry.created()) {
+ recordUpdate(term, clock.getTimestamp());
+ return updated;
+ } else {
+ LOGGER.debug("Trying to update more recent flow entry {} (stored: {})", updated, stored);
+ return stored;
+ }
+ }
+ }
+ return stored;
+ });
+ }
+
+ /**
+ * Applies the given update function to the rule.
+ *
+ * @param rule the rule to update
+ * @param function the update function to apply
+ * @param term the term in which the change occurred
+ * @param clock the logical clock
+ * @param <T> the result type
+ * @return the update result or {@code null} if the rule was not updated
+ */
+ public <T> T update(FlowRule rule, Function<StoredFlowEntry, T> function, long term, LogicalClock clock) {
+ Map<StoredFlowEntry, StoredFlowEntry> flowEntries = flowBucket.get(rule.id());
+ if (flowEntries == null) {
+ flowEntries = flowBucket.computeIfAbsent(rule.id(), id -> Maps.newConcurrentMap());
+ }
+
+ AtomicReference<T> resultRef = new AtomicReference<>();
+ flowEntries.computeIfPresent(new DefaultFlowEntry(rule), (k, stored) -> {
+ if (stored != null) {
+ T result = function.apply(stored);
+ if (result != null) {
+ recordUpdate(term, clock.getTimestamp());
+ resultRef.set(result);
+ }
+ }
+ return stored;
+ });
+ return resultRef.get();
+ }
+
+ /**
+ * Removes the given flow rule from the bucket.
+ *
+ * @param rule the rule to remove
+ * @param term the term in which the change occurred
+ * @param clock the logical clock
+ * @return the removed flow entry
+ */
+ public FlowEntry remove(FlowEntry rule, long term, LogicalClock clock) {
+ final AtomicReference<FlowEntry> removedRule = new AtomicReference<>();
+ flowBucket.computeIfPresent(rule.id(), (flowId, flowEntries) -> {
+ flowEntries.computeIfPresent((StoredFlowEntry) rule, (k, stored) -> {
+ if (rule instanceof DefaultFlowEntry) {
+ DefaultFlowEntry toRemove = (DefaultFlowEntry) rule;
+ if (stored instanceof DefaultFlowEntry) {
+ DefaultFlowEntry storedEntry = (DefaultFlowEntry) stored;
+ if (toRemove.created() < storedEntry.created()) {
+ LOGGER.debug("Trying to remove more recent flow entry {} (stored: {})", toRemove, stored);
+ // the key is not updated, removedRule remains null
+ return stored;
+ }
+ }
+ }
+ removedRule.set(stored);
+ return null;
+ });
+ return flowEntries.isEmpty() ? null : flowEntries;
+ });
+
+ if (removedRule.get() != null) {
+ recordUpdate(term, clock.getTimestamp());
+ return removedRule.get();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Clears the bucket.
+ */
+ public void clear() {
+ term = 0;
+ timestamp = new LogicalTimestamp(0);
+ flowBucket.clear();
+ }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucketDigest.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucketDigest.java
new file mode 100644
index 0000000..b5453e5
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/FlowBucketDigest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.Objects;
+
+import org.onosproject.store.LogicalTimestamp;
+
+/**
+ * Flow bucket digest.
+ */
+public class FlowBucketDigest {
+ private final int bucket;
+ private final long term;
+ private final LogicalTimestamp timestamp;
+
+ FlowBucketDigest(int bucket, long term, LogicalTimestamp timestamp) {
+ this.bucket = bucket;
+ this.term = term;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Returns the bucket identifier.
+ *
+ * @return the bucket identifier
+ */
+ public int bucket() {
+ return bucket;
+ }
+
+ /**
+ * Returns the bucket term.
+ *
+ * @return the bucket term
+ */
+ public long term() {
+ return term;
+ }
+
+ /**
+ * Returns the bucket timestamp.
+ *
+ * @return the bucket timestamp
+ */
+ public LogicalTimestamp timestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Returns a boolean indicating whether this digest is newer than the given digest.
+ *
+ * @param digest the digest to check
+ * @return indicates whether this digest is newer than the given digest
+ */
+ public boolean isNewerThan(FlowBucketDigest digest) {
+ return digest == null || term() > digest.term() || timestamp().isNewerThan(digest.timestamp());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bucket);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return object instanceof FlowBucketDigest
+ && ((FlowBucketDigest) object).bucket == bucket;
+ }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEvent.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEvent.java
new file mode 100644
index 0000000..29672d0
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import org.onosproject.event.AbstractEvent;
+
+/**
+ * Flow table lifecycle event.
+ */
+public class LifecycleEvent extends AbstractEvent<LifecycleEvent.Type, DeviceReplicaInfo> {
+
+ /**
+ * Lifecycle event type.
+ */
+ public enum Type {
+ TERM_START,
+ TERM_ACTIVE,
+ TERM_END,
+ }
+
+ public LifecycleEvent(Type type, DeviceReplicaInfo subject) {
+ super(type, subject);
+ }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEventListener.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEventListener.java
new file mode 100644
index 0000000..a2f3b3d
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleEventListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import org.onosproject.event.EventListener;
+
+/**
+ * Flow table lifecycle event listener.
+ */
+public interface LifecycleEventListener extends EventListener<LifecycleEvent> {
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleManager.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleManager.java
new file mode 100644
index 0000000..213454c
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LifecycleManager.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import org.onosproject.event.ListenerService;
+
+/**
+ * Flow table lifecycle manager.
+ */
+public interface LifecycleManager extends ListenerService<LifecycleEvent, LifecycleEventListener> {
+
+ /**
+ * Returns the current term.
+ *
+ * @return the current term
+ */
+ DeviceReplicaInfo getReplicaInfo();
+
+ /**
+ * Activates the given term.
+ *
+ * @param term the term to activate
+ */
+ void activate(long term);
+
+ /**
+ * Closes the lifecycle manager.
+ */
+ void close();
+
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LogicalClock.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LogicalClock.java
new file mode 100644
index 0000000..92d39cf
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/LogicalClock.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.onosproject.store.LogicalTimestamp;
+
+/**
+ * Logical clock.
+ */
+public final class LogicalClock {
+ private final AtomicLong timestamp = new AtomicLong();
+
+ /**
+ * Records an event.
+ *
+ * @param timestamp the event timestamp
+ */
+ public void tick(LogicalTimestamp timestamp) {
+ this.timestamp.accumulateAndGet(timestamp.value(), (x, y) -> Math.max(x, y) + 1);
+ }
+
+ /**
+ * Returns a timestamped value.
+ *
+ * @param value the value to timestamp
+ * @param <T> the value type
+ * @return the timestamped value
+ */
+ public <T> Timestamped<T> timestamp(T value) {
+ return new Timestamped<>(value, getTimestamp());
+ }
+
+ /**
+ * Increments and returns the current timestamp.
+ *
+ * @return the current timestamp
+ */
+ public LogicalTimestamp getTimestamp() {
+ return new LogicalTimestamp(timestamp.incrementAndGet());
+ }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/Timestamped.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/Timestamped.java
new file mode 100644
index 0000000..e7f566b
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/Timestamped.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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 org.onosproject.store.flow.impl;
+
+import org.onosproject.store.LogicalTimestamp;
+
+/**
+ * Timestamped value.
+ */
+public class Timestamped<T> {
+ private final T value;
+ private final LogicalTimestamp timestamp;
+
+ public Timestamped(T value, LogicalTimestamp timestamp) {
+ this.value = value;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Returns the value.
+ *
+ * @return the value
+ */
+ public T value() {
+ return value;
+ }
+
+ /**
+ * Returns the timestamp.
+ *
+ * @return the timestamp
+ */
+ public LogicalTimestamp timestamp() {
+ return timestamp;
+ }
+}
diff --git a/core/store/dist/src/test/java/org/onosproject/store/flow/impl/ECFlowRuleStoreTest.java b/core/store/dist/src/test/java/org/onosproject/store/flow/impl/ECFlowRuleStoreTest.java
index 7fd0412..72562f9 100644
--- a/core/store/dist/src/test/java/org/onosproject/store/flow/impl/ECFlowRuleStoreTest.java
+++ b/core/store/dist/src/test/java/org/onosproject/store/flow/impl/ECFlowRuleStoreTest.java
@@ -16,6 +16,7 @@
package org.onosproject.store.flow.impl;
import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import org.junit.After;
import org.junit.Before;
@@ -26,6 +27,7 @@
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.ControllerNode;
import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.mastership.MastershipInfo;
import org.onosproject.mastership.MastershipServiceAdapter;
import org.onosproject.net.device.DeviceServiceAdapter;
import org.onosproject.net.DeviceId;
@@ -40,10 +42,16 @@
import org.onosproject.net.intent.IntentTestsMocks;
import org.onosproject.store.cluster.messaging.ClusterCommunicationServiceAdapter;
import org.onosproject.store.persistence.PersistenceServiceAdapter;
+import org.onosproject.store.service.AsyncConsistentMap;
+import org.onosproject.store.service.AsyncConsistentMapAdapter;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.ConsistentMapBuilder;
import org.onosproject.store.service.TestStorageService;
import org.onlab.packet.Ip4Address;
import java.util.Iterator;
+import java.util.Optional;
+
import org.osgi.service.component.ComponentContext;
import static org.easymock.EasyMock.createMock;
@@ -103,6 +111,16 @@
public NodeId getMasterFor(DeviceId deviceId) {
return new NodeId("1");
}
+
+ @Override
+ public MastershipInfo getMastershipFor(DeviceId deviceId) {
+ return new MastershipInfo(
+ 1,
+ Optional.of(NodeId.nodeId("1")),
+ ImmutableMap.<NodeId, MastershipRole>builder()
+ .put(NodeId.nodeId("1"), MastershipRole.MASTER)
+ .build());
+ }
}
@@ -132,8 +150,27 @@
@Before
public void setUp() throws Exception {
flowStoreImpl = new ECFlowRuleStore();
- flowStoreImpl.storageService = new TestStorageService();
- flowStoreImpl.replicaInfoManager = new ReplicaInfoManager();
+ flowStoreImpl.storageService = new TestStorageService() {
+ @Override
+ public <K, V> ConsistentMapBuilder<K, V> consistentMapBuilder() {
+ return new ConsistentMapBuilder<K, V>() {
+ @Override
+ public AsyncConsistentMap<K, V> buildAsyncMap() {
+ return new AsyncConsistentMapAdapter<K, V>();
+ }
+
+ @Override
+ public ConsistentMap<K, V> build() {
+ return null;
+ }
+ };
+ }
+ };
+
+ ReplicaInfoManager replicaInfoManager = new ReplicaInfoManager();
+ replicaInfoManager.mastershipService = new MasterOfAll();
+
+ flowStoreImpl.replicaInfoManager = replicaInfoManager;
mockClusterService = createMock(ClusterService.class);
flowStoreImpl.clusterService = mockClusterService;
nodeId = new NodeId("1");
diff --git a/utils/misc/src/main/java/org/onlab/util/Tools.java b/utils/misc/src/main/java/org/onlab/util/Tools.java
index caac1c1..1246ec9 100644
--- a/utils/misc/src/main/java/org/onlab/util/Tools.java
+++ b/utils/misc/src/main/java/org/onlab/util/Tools.java
@@ -755,6 +755,31 @@
}
/**
+ * Returns a new CompletableFuture completed with the first result from a list of futures. If no future
+ * is completed successfully, the returned future will be completed with the first exception.
+ *
+ * @param futures the input futures
+ * @param <T> future result type
+ * @return a new CompletableFuture
+ */
+ public static <T> CompletableFuture<T> firstOf(List<CompletableFuture<T>> futures) {
+ CompletableFuture<T> resultFuture = new CompletableFuture<>();
+ CompletableFuture.allOf(futures.stream()
+ .map(future -> future.thenAccept(r -> resultFuture.complete(r)))
+ .toArray(CompletableFuture[]::new))
+ .whenComplete((r, e) -> {
+ if (!resultFuture.isDone()) {
+ if (e != null) {
+ resultFuture.completeExceptionally(e);
+ } else {
+ resultFuture.complete(null);
+ }
+ }
+ });
+ return resultFuture;
+ }
+
+ /**
* Returns a new CompletableFuture completed by with the first positive result from a list of
* input CompletableFutures.
*