org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeState.java Source code

Java tutorial

Introduction

Here is the source code for org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeState.java

Source

/*
 * Copyright 2016-present Open Networking Laboratory
 *
 * 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.primitives.resources.impl;

import static org.slf4j.LoggerFactory.getLogger;
import io.atomix.copycat.server.Commit;
import io.atomix.copycat.server.Snapshottable;
import io.atomix.copycat.server.StateMachineExecutor;
import io.atomix.copycat.server.session.ServerSession;
import io.atomix.copycat.server.session.SessionListener;
import io.atomix.copycat.server.storage.snapshot.SnapshotReader;
import io.atomix.copycat.server.storage.snapshot.SnapshotWriter;
import io.atomix.resource.ResourceStateMachine;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import org.onlab.util.Match;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.Clear;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.Get;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.GetChildren;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.Listen;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.Unlisten;
import org.onosproject.store.primitives.resources.impl.AtomixDocumentTreeCommands.Update;
import org.onosproject.store.primitives.resources.impl.DocumentTreeUpdateResult.Status;
import org.onosproject.store.service.DocumentPath;
import org.onosproject.store.service.DocumentTree;
import org.onosproject.store.service.DocumentTreeEvent;
import org.onosproject.store.service.DocumentTreeEvent.Type;
import org.onosproject.store.service.IllegalDocumentModificationException;
import org.onosproject.store.service.NoSuchDocumentPathException;
import org.onosproject.store.service.Versioned;
import org.slf4j.Logger;

import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;

/**
 * State Machine for {@link AtomixDocumentTree} resource.
 */
public class AtomixDocumentTreeState extends ResourceStateMachine implements SessionListener, Snapshottable {

    private final Logger log = getLogger(getClass());
    private final Map<Long, SessionListenCommits> listeners = new HashMap<>();
    private AtomicLong versionCounter = new AtomicLong(0);
    private final DocumentTree<TreeNodeValue> docTree = new DefaultDocumentTree<>(versionCounter::incrementAndGet);

    public AtomixDocumentTreeState(Properties properties) {
        super(properties);
    }

    @Override
    public void snapshot(SnapshotWriter writer) {
        writer.writeLong(versionCounter.get());
    }

    @Override
    public void install(SnapshotReader reader) {
        versionCounter = new AtomicLong(reader.readLong());
    }

    @Override
    protected void configure(StateMachineExecutor executor) {
        // Listeners
        executor.register(Listen.class, this::listen);
        executor.register(Unlisten.class, this::unlisten);
        // queries
        executor.register(Get.class, this::get);
        executor.register(GetChildren.class, this::getChildren);
        // commands
        executor.register(Update.class, this::update);
        executor.register(Clear.class, this::clear);
    }

    protected void listen(Commit<? extends Listen> commit) {
        Long sessionId = commit.session().id();
        listeners.computeIfAbsent(sessionId, k -> new SessionListenCommits()).add(commit);
        commit.session().onStateChange(state -> {
            if (state == ServerSession.State.CLOSED || state == ServerSession.State.EXPIRED) {
                closeListener(commit.session().id());
            }
        });
    }

    protected void unlisten(Commit<? extends Unlisten> commit) {
        Long sessionId = commit.session().id();
        try {
            SessionListenCommits listenCommits = listeners.get(sessionId);
            if (listenCommits != null) {
                listenCommits.remove(commit);
            }
        } finally {
            commit.close();
        }
    }

    protected Versioned<byte[]> get(Commit<? extends Get> commit) {
        try {
            Versioned<TreeNodeValue> value = docTree.get(commit.operation().path());
            return value == null ? null : value.map(node -> node == null ? null : node.value());
        } catch (IllegalStateException e) {
            return null;
        } finally {
            commit.close();
        }
    }

    protected Map<String, Versioned<byte[]>> getChildren(Commit<? extends GetChildren> commit) {
        try {
            Map<String, Versioned<TreeNodeValue>> children = docTree.getChildren(commit.operation().path());
            return children == null ? null
                    : Maps.newHashMap(Maps.transformValues(children, value -> value.map(TreeNodeValue::value)));
        } finally {
            commit.close();
        }
    }

    protected DocumentTreeUpdateResult<byte[]> update(Commit<? extends Update> commit) {
        DocumentTreeUpdateResult<byte[]> result = null;
        DocumentPath path = commit.operation().path();
        boolean updated = false;
        Versioned<TreeNodeValue> currentValue = docTree.get(path);
        try {
            Match<Long> versionMatch = commit.operation().versionMatch();
            Match<byte[]> valueMatch = commit.operation().valueMatch();

            if (versionMatch.matches(currentValue == null ? null : currentValue.version())
                    && valueMatch.matches(currentValue == null ? null : currentValue.value().value())) {
                if (commit.operation().value() == null) {
                    docTree.removeNode(path);
                } else {
                    docTree.set(path, new NonTransactionalCommit(commit));
                }
                updated = true;
            }
            Versioned<TreeNodeValue> newValue = updated ? docTree.get(path) : currentValue;
            Status updateStatus = updated ? Status.OK
                    : commit.operation().value() == null ? Status.INVALID_PATH : Status.NOOP;
            result = new DocumentTreeUpdateResult<>(path, updateStatus,
                    newValue == null ? null : newValue.map(TreeNodeValue::value),
                    currentValue == null ? null : currentValue.map(TreeNodeValue::value));
        } catch (IllegalDocumentModificationException e) {
            result = DocumentTreeUpdateResult.illegalModification(path);
        } catch (NoSuchDocumentPathException e) {
            result = DocumentTreeUpdateResult.invalidPath(path);
        } catch (Exception e) {
            log.error("Failed to apply {} to state machine", commit.operation(), e);
            throw Throwables.propagate(e);
        } finally {
            if (updated) {
                if (currentValue != null) {
                    currentValue.value().discard();
                }
            } else {
                commit.close();
            }
        }
        notifyListeners(path, result);
        return result;
    }

    protected void clear(Commit<? extends Clear> commit) {
        try {
            Queue<DocumentPath> toClearQueue = Queues.newArrayDeque();
            Map<String, Versioned<TreeNodeValue>> topLevelChildren = docTree.getChildren(DocumentPath.from("root"));
            toClearQueue.addAll(topLevelChildren.keySet().stream()
                    .map(name -> new DocumentPath(name, DocumentPath.from("root"))).collect(Collectors.toList()));
            while (!toClearQueue.isEmpty()) {
                DocumentPath path = toClearQueue.remove();
                Map<String, Versioned<TreeNodeValue>> children = docTree.getChildren(path);
                if (children.size() == 0) {
                    docTree.removeNode(path).value().discard();
                } else {
                    children.keySet().stream().forEach(name -> toClearQueue.add(new DocumentPath(name, path)));
                    toClearQueue.add(path);
                }
            }
        } finally {
            commit.close();
        }
    }

    /**
     * Interface implemented by tree node values.
     */
    private interface TreeNodeValue {
        /**
         * Returns the raw {@code byte[]}.
         *
         * @return raw value
         */
        byte[] value();

        /**
         * Discards the value by invoke appropriate clean up actions.
         */
        void discard();
    }

    /**
     * A {@code TreeNodeValue} that is derived from a non-transactional update
     * i.e. via any standard tree update operation.
     */
    private class NonTransactionalCommit implements TreeNodeValue {
        private final Commit<? extends Update> commit;

        public NonTransactionalCommit(Commit<? extends Update> commit) {
            this.commit = commit;
        }

        @Override
        public byte[] value() {
            return commit.operation().value().orElse(null);
        }

        @Override
        public void discard() {
            commit.close();
        }
    }

    private void notifyListeners(DocumentPath path, DocumentTreeUpdateResult<byte[]> result) {
        if (result.status() != Status.OK) {
            return;
        }
        DocumentTreeEvent<byte[]> event = new DocumentTreeEvent<>(path,
                result.created() ? Type.CREATED : result.newValue() == null ? Type.DELETED : Type.UPDATED,
                Optional.ofNullable(result.newValue()), Optional.ofNullable(result.oldValue()));

        listeners.values().stream().filter(l -> event.path().isDescendentOf(l.leastCommonAncestorPath()))
                .forEach(listener -> listener.publish(AtomixDocumentTree.CHANGE_SUBJECT, Arrays.asList(event)));
    }

    @Override
    public void register(ServerSession session) {
    }

    @Override
    public void unregister(ServerSession session) {
        closeListener(session.id());
    }

    @Override
    public void expire(ServerSession session) {
        closeListener(session.id());
    }

    @Override
    public void close(ServerSession session) {
        closeListener(session.id());
    }

    private void closeListener(Long sessionId) {
        SessionListenCommits listenCommits = listeners.remove(sessionId);
        if (listenCommits != null) {
            listenCommits.close();
        }
    }

    private class SessionListenCommits {
        private final List<Commit<? extends Listen>> commits = Lists.newArrayList();
        private DocumentPath leastCommonAncestorPath;

        public void add(Commit<? extends Listen> commit) {
            commits.add(commit);
            recomputeLeastCommonAncestor();
        }

        public void remove(Commit<? extends Unlisten> commit) {
            // Remove the first listen commit with path matching path in unlisten commit
            Iterator<Commit<? extends Listen>> iterator = commits.iterator();
            while (iterator.hasNext()) {
                Commit<? extends Listen> listenCommit = iterator.next();
                if (listenCommit.operation().path().equals(commit.operation().path())) {
                    iterator.remove();
                    listenCommit.close();
                }
            }
            recomputeLeastCommonAncestor();
        }

        public DocumentPath leastCommonAncestorPath() {
            return leastCommonAncestorPath;
        }

        public <M> void publish(String topic, M message) {
            commits.stream().findAny().ifPresent(commit -> commit.session().publish(topic, message));
        }

        public void close() {
            commits.forEach(Commit::close);
            commits.clear();
            leastCommonAncestorPath = null;
        }

        private void recomputeLeastCommonAncestor() {
            this.leastCommonAncestorPath = DocumentPath.leastCommonAncestor(
                    commits.stream().map(c -> c.operation().path()).collect(Collectors.toList()));
        }
    }
}