/*
* Copyright 2015 the original author or authors.
*
* 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 io.atomix.resource;
import io.atomix.catalyst.util.Assert;
import io.atomix.copycat.server.Commit;
import io.atomix.copycat.server.StateMachine;
import io.atomix.copycat.server.StateMachineExecutor;
import io.atomix.copycat.server.session.ServerSession;
import io.atomix.copycat.server.session.SessionListener;
import io.atomix.resource.internal.ResourceCommand;
import io.atomix.resource.internal.ResourceEvent;
import io.atomix.resource.internal.ResourceQuery;
import java.util.*;
/**
* Base class for resource state machines.
* <p>
* Resource implementations should extend the resource state machine for support in Atomix core.
* This state machine implements functionality necessary to multiplexing the state machine on a
* single Copycat log and provides facilities for configuring and cleaning up the resource upon
* deletion.
* <p>
* To implement a resource state machine, simply extend this class. State machines must adhere
* to the same rules as those outlined in the Copycat documentation. See the {@link StateMachine}
* documentation for more information.
* <pre>
* {@code
* public class MapStateMachine extends ResourceStateMachine {
* private final Map<Object, Object> map = new HashMap<>();
*
* public void put(Commit<PutCommand> commit) {
* try {
* map.put(commit.operation().key(), commit.operation().value());
* } finally {
* commit.close();
* }
* }
* }
* }
* </pre>
* Resource state machines should override the {@link #delete()} method to perform cleanup of
* the resource state upon deletion of the resource. Cleanup tasks might include notifying clients
* of the deletion of the resource or releasing {@link Commit commits} held by the state machine.
* <pre>
* {@code
* public class MapStateMachine extends ResourceStateMachine {
* private final Map<Object, Commit<PutCommand>> map = new HashMap<>();
*
* public void put(Commit<PutCommand> commit) {
* map.put(commit.operation().key(), commit);
* }
*
* public void delete() {
* for (Map.Entry<Object, Commit<PutCommand>> entry : map.entrySet()) {
* entry.getValue().close();
* }
* map.clear();
* }
* }
* }
* </pre>
* Resource state machines implement Copycat's {@link SessionListener} interface to allow state
* machines to listen for changes in client sessions. When used in an Atomix cluster, sessions
* are specific to a state machine and not the entire cluster. When a client or replica opens
* a resource, a new logical session to that resource is opened and the session listener's
* {@link SessionListener#register(ServerSession)} method will be called. When a client or replica
* closes a resource, the {@link SessionListener#close(ServerSession)} method will be called.
* In other words, sessions in resource state machines represent a client's open resource instance,
* and event messages {@link ServerSession#publish(String, Object) published} to that session will
* be received by the specific client side {@link Resource} instance.
* <pre>
* {@code
* public class MapStateMachine extends ResourceStateMachine {
* private final Map<Object, Object> map = new HashMap<>();
* private final Set<ServerSession> listeners = new HashSet<>();
*
* public void listen(Commit<ListenCommand> commit) {
* listeners.add(commit.session());
* }
*
* public void put(Commit<PutCommand> commit) {
* map.put(commit.operation().key(), commit.operation().value());
* for (ServerSession listener : listeners) {
* listener.publish(commit.operation().key(), commit.operation().value());
* }
* }
*
* public void close(ServerSession session) {
* listeners.remove(session);
* }
* }
* }
* </pre>
*
* @author <a href="http://github.com/kuujo>Jordan Halterman</a>
*/
public abstract class ResourceStateMachine extends StateMachine implements SessionListener {
protected final Properties config;
private final Map<Integer, Set<ServerSession>> eventListeners = new HashMap<>();
protected ResourceStateMachine(Properties config) {
this.config = Assert.notNull(config, "config");
}
@Override
public final void init(StateMachineExecutor executor) {
executor.serializer().register(ResourceCommand.class, -50);
executor.serializer().register(ResourceQuery.class, -51);
executor.serializer().register(ResourceQuery.Config.class, -52);
executor.serializer().register(ResourceCommand.Delete.class, -53);
executor.serializer().register(ResourceEvent.class, -49);
executor.context().sessions().addListener(this);
ResourceStateMachineExecutor wrappedExecutor = new ResourceStateMachineExecutor(executor);
wrappedExecutor.register(ResourceQuery.Config.class, this::config);
wrappedExecutor.<ResourceCommand.Register>register(ResourceCommand.Register.class, this::register);
wrappedExecutor.<ResourceCommand.Unregister>register(ResourceCommand.Unregister.class, this::unregister);
wrappedExecutor.<ResourceCommand.Delete>register(ResourceCommand.Delete.class, this::delete);
super.init(wrappedExecutor);
}
@Override
public void register(ServerSession session) {
}
@Override
public void unregister(ServerSession session) {
}
@Override
public void expire(ServerSession session) {
}
@Override
public void close(ServerSession session) {
}
/**
* Notifies all subscribed clients of an event.
*
* @param event The event message.
*/
protected void notify(Resource.Event event) {
Set<ServerSession> sessions = eventListeners.get(event.type().id());
if (sessions != null) {
for (ServerSession session : sessions) {
session.publish("event", new ResourceEvent(event.type().id(), event));
}
}
}
/**
* Returns the resource configuration.
*/
private Properties config(Commit<ResourceQuery.Config> commit) {
try {
return config;
} finally {
commit.close();
}
}
/**
* Registers an event listener.
*/
private void register(Commit<ResourceCommand.Register> commit) {
Set<ServerSession> sessions = eventListeners.computeIfAbsent(commit.command().event(), k -> new HashSet<>());
sessions.add(commit.session());
}
/**
* Unregisters an event listener.
*/
private void unregister(Commit<ResourceCommand.Unregister> commit) {
Set<ServerSession> sessions = eventListeners.computeIfAbsent(commit.command().event(), k -> new HashSet<>());
sessions.remove(commit.session());
if (sessions.isEmpty()) {
eventListeners.remove(commit.command().event());
}
}
/**
* Handles a delete command.
*/
private void delete(Commit<ResourceCommand.Delete> commit) {
try {
delete();
} finally {
commit.close();
}
}
/**
* Cleans up deleted state machine state.
* <p>
* This method is called when a client {@link Resource#delete() deletes} a resource in the cluster.
* This method is guaranteed to eventually be called on all servers at the same point in logical time
* (i.e. {@code index}). State machines should override this method to clean up state machine state,
* including releasing {@link Commit}s held by the state machine.
* <pre>
* {@code
* public class MapStateMachine extends ResourceStateMachine {
* private final Map<Object, Commit<PutCommand>> map = new HashMap<>();
*
* public void put(Commit<PutCommand> commit) {
* map.put(commit.operation().key(), commit);
* }
*
* public void delete() {
* for (Map.Entry<Object, Commit<PutCommand>> entry : map.entrySet()) {
* entry.getValue().close();
* }
* map.clear();
* }
* }
* }
* </pre>
* Failing to override this method to clean up commits held by a resource state machine is considered
* a bug and will eventually result in disk filling up.
*/
public void delete() {
}
}