/* * 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.collections.internal; import io.atomix.catalyst.concurrent.Scheduled; import io.atomix.copycat.server.Commit; import io.atomix.copycat.server.session.ServerSession; import io.atomix.resource.ResourceStateMachine; import java.time.Duration; import java.util.*; import static io.atomix.collections.DistributedMap.EntryEvent; import static io.atomix.collections.DistributedMap.Events; /** * Map state machine. * * @author <a href="http://github.com/kuujo">Jordan Halterman</a> */ public class MapState extends ResourceStateMachine { private final Map<Object, Value> map = new HashMap<>(); private final Map<Object, Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>>> listeners = new HashMap<>(); public MapState(Properties config) { super(config); } @Override public void close(ServerSession session) { // Remove the session from event listeners. Iterator<Map.Entry<Object, Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>>>> keyIterator = listeners.entrySet().iterator(); while (keyIterator.hasNext()) { Map.Entry<Object, Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>>> keyEntry = keyIterator.next(); Iterator<Map.Entry<Integer, Map<Long, Commit<MapCommands.KeyListen>>>> eventIterator = keyEntry.getValue().entrySet().iterator(); while (eventIterator.hasNext()) { Map.Entry<Integer, Map<Long, Commit<MapCommands.KeyListen>>> eventEntry = eventIterator.next(); Map<Long, Commit<MapCommands.KeyListen>> sessions = eventEntry.getValue(); Commit<MapCommands.KeyListen> commit = sessions.remove(session.id()); if (commit != null) { commit.release(); if (sessions.isEmpty()) { eventIterator.remove(); } } } if (keyEntry.getValue().isEmpty()) { keyIterator.remove(); } } } /** * Notifies clients of an entry event. * * @param event The entry event. */ protected void notify(EntryEvent event) { Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>> keyListeners = listeners.get(event.entry().getKey()); if (keyListeners != null) { Map<Long, Commit<MapCommands.KeyListen>> eventListeners = keyListeners.get(event.type().id()); if (eventListeners != null) { for (Commit<MapCommands.KeyListen> listener : eventListeners.values()) { listener.session().publish("key", event); } } } super.notify(event); } /** * Registers a key change listener. */ public void listen(Commit<MapCommands.KeyListen> commit) { Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>> listeners = this.listeners.computeIfAbsent(commit.command().key(), k -> new HashMap<>()); Map<Long, Commit<MapCommands.KeyListen>> sessions = listeners.computeIfAbsent(commit.command().event(), e -> new HashMap<>()); if (!sessions.containsKey(commit.session().id())) { sessions.put(commit.session().id(), commit); } else { commit.release(); } } /** * Unregisters a key change listener. */ public void unlisten(Commit<MapCommands.KeyUnlisten> commit) { try { Map<Integer, Map<Long, Commit<MapCommands.KeyListen>>> listeners = this.listeners.get(commit.command().key()); if (listeners != null) { Map<Long, Commit<MapCommands.KeyListen>> sessions = listeners.get(commit.command().event()); if (sessions != null) { Commit<MapCommands.KeyListen> listen = sessions.remove(commit.session().id()); if (listen != null) { listen.release(); if (sessions.isEmpty()) { listeners.remove(commit.command().event()); if (listeners.isEmpty()) { this.listeners.remove(commit.command().key()); } } } } } } finally { commit.release(); } } /** * Handles a contains key commit. */ public boolean containsKey(Commit<MapCommands.ContainsKey> commit) { try { return map.containsKey(commit.operation().key()); } finally { commit.close(); } } /** * Handles a contains value commit. */ public boolean containsValue(Commit<MapCommands.ContainsValue> commit) { try { for (Value value : map.values()) { if (value.commit.operation().value().equals(commit.operation().value())) { return true; } } return false; } finally { commit.close(); } } /** * Handles a get commit. */ public Object get(Commit<MapCommands.Get> commit) { try { Value value = map.get(commit.operation().key()); return value != null ? value.commit.operation().value() : null; } finally { commit.close(); } } /** * Handles a get or default commit. */ public Object getOrDefault(Commit<MapCommands.GetOrDefault> commit) { try { Value value = map.get(commit.operation().key()); return value != null ? value.commit.operation().value() : commit.operation().defaultValue(); } finally { commit.close(); } } /** * Handles a put commit. */ public Object put(Commit<MapCommands.Put> commit) { try { final Object key = commit.command().key(); final long ttl = commit.command().ttl(); final Scheduled timer = ttl > 0 ? executor.schedule(Duration.ofMillis(ttl), () -> { Value removed = map.remove(key); if (removed != null) { notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(key, removed.commit.command().value()))); removed.commit.close(); } }) : null; Value value = map.put(key, new Value(commit, timer)); if (value != null) { try { if (value.timer != null) value.timer.cancel(); notify(new EntryEvent<>(Events.UPDATE, new MapEntry<>(key, commit.operation().value()))); return value.commit.operation().value(); } finally { value.commit.close(); } } else { notify(new EntryEvent<>(Events.ADD, new MapEntry<>(key, commit.operation().value()))); } return null; } catch (Exception e) { commit.close(); throw e; } } /** * Handles a put if absent commit. */ public Object putIfAbsent(Commit<MapCommands.PutIfAbsent> commit) { try { final Object key = commit.command().key(); final long ttl = commit.command().ttl(); final Value value = map.get(key); if (value == null) { final Scheduled timer = ttl > 0 ? executor.schedule(Duration.ofMillis(ttl), () -> { Value removed = map.remove(key); if (removed != null) { notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(key, removed.commit.command().value()))); removed.commit.close(); } }) : null; map.put(key, new Value(commit, timer)); notify(new EntryEvent<>(Events.ADD, new MapEntry<>(key, commit.command().value()))); return null; } else { commit.close(); return value.commit.command().value(); } } catch (Exception e) { commit.close(); throw e; } } /** * Handles a remove commit. */ public Object remove(Commit<MapCommands.Remove> commit) { try { Value value = map.remove(commit.operation().key()); if (value != null) { try { if (value.timer != null) value.timer.cancel(); notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(value.commit.operation().key(), value.commit.operation().value()))); return value.commit.operation().value(); } finally { value.commit.close(); } } return null; } finally { commit.close(); } } /** * Handles a remove if present commit. */ public boolean removeIfPresent(Commit<MapCommands.RemoveIfPresent> commit) { try { Value value = map.get(commit.operation().key()); if (value == null || ((value.commit.operation().value() == null && commit.operation().value() != null) || (value.commit.operation().value() != null && !value.commit.operation().value().equals(commit.operation().value())))) { return false; } else { try { map.remove(commit.operation().key()); if (value.timer != null) value.timer.cancel(); notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(value.commit.operation().key(), value.commit.operation().value()))); return true; } finally { value.commit.close(); } } } finally { commit.close(); } } /** * Handles a replace commit. */ public Object replace(Commit<MapCommands.Replace> commit) { Value value = map.get(commit.operation().key()); if (value != null) { try { final Object key = commit.command().key(); final long ttl = commit.command().ttl(); if (value.timer != null) value.timer.cancel(); final Scheduled timer = ttl > 0 ? executor.schedule(Duration.ofMillis(ttl), () -> { Value removed = map.remove(key); if (removed != null) { notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(key, removed.commit.command().value()))); removed.commit.close(); } }) : null; map.put(key, new Value(commit, timer)); notify(new EntryEvent<>(Events.UPDATE, new MapEntry<>(key, commit.command().value()))); return value.commit.operation().value(); } finally { value.commit.close(); } } else { commit.close(); } return null; } /** * Handles a replace if present commit. */ public boolean replaceIfPresent(Commit<MapCommands.ReplaceIfPresent> commit) { final Object key = commit.command().key(); Value value = map.get(key); if (value == null) { commit.close(); return false; } if ((value.commit.operation().value() == null && commit.operation().replace() == null) || (value.commit.operation().value() != null && value.commit.operation().value().equals(commit.operation().replace()))) { if (value.timer != null) value.timer.cancel(); final long ttl = commit.command().ttl(); final Scheduled timer = ttl > 0 ? executor.schedule(Duration.ofMillis(ttl), () -> { Value removed = map.remove(key); if (removed != null) { notify(new EntryEvent<>(Events.REMOVE, new MapEntry<>(key, removed.commit.command().value()))); removed.commit.close(); } }) : null; map.put(key, new Value(commit, timer)); notify(new EntryEvent<>(Events.UPDATE, new MapEntry<>(key, commit.operation().value()))); value.commit.close(); return true; } else { commit.close(); } return false; } /** * Handles a values query. */ public Collection<Object> values(Commit<MapCommands.Values> commit) { try { Collection<Object> values = new ArrayList<>(); for (Value value : map.values()) { values.add(value.commit.operation().value()); } return values; } finally { commit.close(); } } /** * Handles a key set query. */ public Set<Object> keySet(Commit<MapCommands.KeySet> commit) { try { return new HashSet<>(map.keySet()); } finally { commit.close(); } } /** * Handles an entry set query. */ public Set<Map.Entry<Object, Object>> entrySet(Commit<MapCommands.EntrySet> commit) { try { Set<Map.Entry<Object, Object>> entries = new HashSet<>(); for (Map.Entry<Object, Value> entry : map.entrySet()) { entries.add(new MapEntry<>(entry.getKey(), entry.getValue().commit.operation().value())); } return entries; } finally { commit.close(); } } /** * Handles a count commit. */ public int size(Commit<MapCommands.Size> commit) { try { return map.size(); } finally { commit.close(); } } /** * Handles an is empty commit. */ public boolean isEmpty(Commit<MapCommands.IsEmpty> commit) { try { return map == null || map.isEmpty(); } finally { commit.close(); } } /** * Handles a clear commit. */ public void clear(Commit<MapCommands.Clear> commit) { try { delete(); } finally { commit.close(); } } @Override public void delete() { Iterator<Map.Entry<Object, Value>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Object, Value> entry = iterator.next(); Value value = entry.getValue(); if (value.timer != null) value.timer.cancel(); value.commit.close(); iterator.remove(); } } /** * Map value. */ private static class Value { private final Commit<? extends MapCommands.TtlCommand> commit; private final Scheduled timer; private Value(Commit<? extends MapCommands.TtlCommand> commit, Scheduled timer) { this.commit = commit; this.timer = timer; } } }