All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.spi.cluster.consul.impl.ConsulAsyncMap Maven / Gradle / Ivy

/*
 * Copyright (C) 2018-2019 Roman Levytskyi
 *
 * 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.vertx.spi.cluster.consul.impl;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.shareddata.AsyncMap;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.core.shareddata.Lock;
import io.vertx.core.spi.cluster.ClusterManager;
import io.vertx.ext.consul.KeyValueOptions;

import java.util.*;

import static io.vertx.core.Future.failedFuture;
import static io.vertx.core.Future.succeededFuture;
import static io.vertx.spi.cluster.consul.impl.ConversationUtils.asFutureString;
import static io.vertx.spi.cluster.consul.impl.ConversationUtils.asTtlConsulEntry;

/**
 * Distributed async map implementation that is backed by consul key-value store.
 * 

* Note: given map is used by vertx nodes to share the data, * entries of this map are always PERSISTENT and NOT EPHEMERAL. * * For ttl handling see {@link TTLMonitor} * * @author Roman Levytskyi */ public class ConsulAsyncMap extends ConsulMap implements AsyncMap { private static final Logger log = LoggerFactory.getLogger(ConsulAsyncMap.class); private final TTLMonitor ttlMonitor; public ConsulAsyncMap(String name, ClusterManagerInternalContext appContext, ClusterManager clusterManager) { super(name, appContext); this.ttlMonitor = new TTLMonitor(appContext.getVertx(), clusterManager, name, appContext.getNodeId()); startListening(); } @Override public void get(K k, Handler> asyncResultHandler) { assertKeyIsNotNull(k) .compose(aVoid -> getValue(k)) .setHandler(asyncResultHandler); } @Override public void put(K k, V v, Handler> completionHandler) { assertKeyAndValueAreNotNull(k, v) .compose(aVoid -> putValue(k, v, null, Optional.empty())) .compose(putSucceeded -> putSucceeded ? Future.succeededFuture() : failedFuture(k.toString() + "wasn't put to: " + name)) .setHandler(completionHandler); } @Override public void put(K k, V v, long ttl, Handler> completionHandler) { assertKeyAndValueAreNotNull(k, v) .compose(id -> putValue(k, v, null, Optional.of(ttl))) .compose(putSucceeded -> putSucceeded ? succeededFuture() : Future.failedFuture(k.toString() + "wasn't put to " + name)) .setHandler(completionHandler); } @Override public void putIfAbsent(K k, V v, Handler> completionHandler) { putIfAbsent(k, v, Optional.empty()).setHandler(completionHandler); } @Override public void putIfAbsent(K k, V v, long ttl, Handler> completionHandler) { assertKeyAndValueAreNotNull(k, v) .compose(aVoid -> putIfAbsent(k, v, Optional.of(ttl))) .setHandler(completionHandler); } @Override public void remove(K k, Handler> asyncResultHandler) { assertKeyIsNotNull(k).compose(aVoid -> { Future future = Future.future(); get(k, future.completer()); return future; }).compose(v -> { Future future = Future.future(); if (v == null) future.complete(); else deleteValueByKeyPath(keyPath(k)) .compose(removeSucceeded -> removeSucceeded ? succeededFuture(v) : failedFuture("Key + " + k + " wasn't removed.")) .setHandler(future.completer()); return future; }).setHandler(asyncResultHandler); } @Override public void removeIfPresent(K k, V v, Handler> resultHandler) { // removes a value from the map, only if entry already exists with same value. assertKeyAndValueAreNotNull(k, v).compose(aVoid -> { Future future = Future.future(); get(k, future.completer()); return future; }).compose(value -> { if (v.equals(value)) return deleteValueByKeyPath(keyPath(k)) .compose(removeSucceeded -> removeSucceeded ? succeededFuture(true) : failedFuture("Key + " + k + " wasn't removed.")); else return succeededFuture(false); }).setHandler(resultHandler); } @Override public void replace(K k, V v, Handler> asyncResultHandler) { // replaces the entry only if it is currently mapped to some value. assertKeyAndValueAreNotNull(k, v).compose(aVoid -> { Future future = Future.future(); get(k, future.completer()); return future; }).compose(value -> { Future future = Future.future(); if (value == null) { future.complete(); } else { put(k, v, event -> { if (event.succeeded()) future.complete(value); else future.fail(event.cause()); }); } return future; }).setHandler(asyncResultHandler); } @Override public void replaceIfPresent(K k, V oldValue, V newValue, Handler> resultHandler) { // replaces the entry only if it is currently mapped to a specific value. assertKeyAndValueAreNotNull(k, oldValue) .compose(aVoid -> assertValueIsNotNull(newValue)) .compose(aVoid -> { Future future = Future.future(); get(k, future.completer()); return future; }) .compose(value -> { Future future = Future.future(); if (value != null) { if (value.equals(oldValue)) put(k, newValue, resultPutHandler -> { if (resultPutHandler.succeeded()) future.complete(true); // old V: '{}' has been replaced by new V: '{}' where K: '{}'", oldValue, newValue, k else future.fail(resultPutHandler.cause()); // failed replace old V: '{}' by new V: '{}' where K: '{}' due to: '{}'", oldValue, newValue, k, resultPutHandler.cause() }); else future.complete(false); // "An entry with K: '{}' doesn't map to old V: '{}' so it won't get replaced.", k, oldValue); } else future.complete(false); // An entry with K: '{}' doesn't exist, return future; }) .setHandler(resultHandler); } @Override public void clear(Handler> resultHandler) { deleteAll().setHandler(resultHandler); } @Override public void size(Handler> resultHandler) { plainKeys().compose(list -> succeededFuture(list.size())).setHandler(resultHandler); } @Override public void keys(Handler>> asyncResultHandler) { entries().compose(kvMap -> succeededFuture(kvMap.keySet())).setHandler(asyncResultHandler); } @Override public void values(Handler>> asyncResultHandler) { entries().compose(kvMap -> Future.>succeededFuture(new ArrayList<>(kvMap.values())).setHandler(asyncResultHandler)); } @Override public void entries(Handler>> asyncResultHandler) { entries().setHandler(asyncResultHandler); } @Override protected void entryUpdated(EntryEvent event) { if (event.getEventType() == EntryEvent.EventType.WRITE) { if (log.isDebugEnabled()) { log.debug("[" + appContext.getNodeId() + "] : " + "applying a ttl monitor on entry: " + event.getEntry().getKey()); } ttlMonitor.apply( event.getEntry().getKey(), asTtlConsulEntry(event.getEntry().getValue())); } } /** * Puts an entry only if there is no entry with the key already present. If key already present then the existing * value will be returned to the handler, otherwise null. * * @param k - holds the entry's key. * @param v - holds the entry's value. * @return future existing value if k is already present, otherwise future null. */ private Future putIfAbsent(K k, V v, Optional ttl) { // set the Check-And-Set index. If the index is {@code 0}, Consul will only put the key if it does not already exist. KeyValueOptions casOpts = new KeyValueOptions().setCasIndex(0); return putValue(k, v, casOpts, ttl).compose(putSucceeded -> { if (putSucceeded) return succeededFuture(); else return getValue(k); // key already present }); } /** * Puts an entry by taking into account TTL. * * @param k - holds the entry's key. * @param v - holds the entry's value. * @param keyValueOptions - cas, this might be null. * @param ttl - ttl on entry in ms. * @return {@link Future} holding the result. */ private Future putValue(K k, V v, KeyValueOptions keyValueOptions, Optional ttl) { Long ttlValue = ttl.map(aLong -> ttl.get()).orElse(null); return asFutureString(k, v, appContext.getNodeId(), ttlValue) .compose(value -> putPlainValue(keyPath(k), value, keyValueOptions)) .compose(result -> ttlMonitor.apply(keyPath(k), ttl) .compose(aVoid -> succeededFuture(result))); } /** * Dedicated to {@link AsyncMap} TTL monitor to handle the ability to place ttl on map's entries. *

* IMPORTANT: * TTL can placed on consul entries by relaying on consul sessions (first we have to register ttl session and bind it with an entry) - once * session gets expired (session's ttl gets expired) -> all entries given session was bound to get removed automatically from consul kv store. * Consul's got following restriction : * - Session TTL value must be between 10s and 86400s -> we can't really rely on using consul sessions since it breaks general * vert.x cluster management SPI. This also should be taken into account: * [Invalidation-time is twice the TTL time](https://github.com/hashicorp/consul/issues/1172) -> actual time when ttl entry gets removed (expired) * is doubled to what you will specify as a ttl. *

* For these reasons custom TTL monitoring mechanism was developed. * * @author Roman Levytskyi */ private static class TTLMonitor { private final static Logger log = LoggerFactory.getLogger(TTLMonitor.class); private final Vertx vertx; private final ClusterManager clusterManager; /* * TTL monitor needs to hold a state - i.e. some sort of correlation between scheduled timers and * corresponding keypaths on which ttl action is gonna get executed. We use vert.x {@link LocalMap} * to hold this state. Having this allows us to either stop OR reschedule an appropriate timer. */ private final LocalMap timerMap; private final String nodeId; private final String mapName; TTLMonitor(Vertx vertx, ClusterManager clusterManager, String mapName, String nodeId) { this.vertx = vertx; this.timerMap = vertx.sharedData().getLocalMap("timerMap"); this.clusterManager = clusterManager; this.mapName = mapName; this.nodeId = nodeId; } /** * Monitors specified keypath on TTL. * If ttl is not present then we attempt to cancel or reschedule the timer (if it was already scheduled on specified keypath.) * If ttl is present we attempt to get a lock and schedule an appropriate vert.x scheduler. * * @param keyPath represents consul key path that will be monitored. * @param ttl time to live in ms. * @return */ Future apply(String keyPath, Optional ttl) { Future future = Future.future(); if (ttl.isPresent()) { if (log.isDebugEnabled()) { log.debug("[" + nodeId + "] : " + "applying ttl monitoring on: " + keyPath + " with ttl: " + ttl.get()); } String lockName = "ttlLockOn/" + keyPath; clusterManager.getLockWithTimeout(lockName, 50, lockObtainedEvent -> { setTimer(keyPath, ttl.get(), lockName, lockObtainedEvent); future.complete(); }); } else { // there's no need to monitor an entry -> no ttl is there. // try to remove keyPath from timerMap cancelTimer(keyPath); future.complete(); } return future; } /** * Schedules a timer for ttl action to take place. * * @param keyPath consul key path. * @param ttl the delay in milliseconds, after which the timer will fire. * @param lockName lock name that is being put to execute the ttl action. * @param lockEvent lock event holding an actual lock. */ private void setTimer(String keyPath, long ttl, String lockName, AsyncResult lockEvent) { long timerId = vertx.setTimer(ttl, event -> { if (lockEvent.succeeded()) { deleteTTLEntry(keyPath, lockEvent.result()); } else { clusterManager.getLockWithTimeout(lockName, 1000, lockObtainedEvent -> { if (lockObtainedEvent.succeeded()) { deleteTTLEntry(keyPath, lockObtainedEvent.result()); } }); } }); timerMap.put(keyPath, timerId); } /** * Cancels vert.x timer and updates timer map accordingly. */ private void cancelTimer(String keyPath) { Long timerId = timerMap.get(keyPath); if (Objects.nonNull(timerId)) { vertx.cancelTimer(timerId); timerMap.remove(keyPath); if (log.isDebugEnabled()) { log.debug("[" + nodeId + "] : " + "cancelling ttl monitoring on entry: " + keyPath); } } } /** * Deletes an entry that ttl was bound to. * We lock delete operation and once delete is executed: * 1) we release the lock. * 2) we update timer map by removing timer id that has triggered delete operation. * * @param keyPath entry's keypath in consul kv store. * @param lock lock holding the delete operation. */ private void deleteTTLEntry(String keyPath, Lock lock) { clusterManager.getAsyncMap(mapName, asyncMapEvent -> { ConsulAsyncMap asyncMap = (ConsulAsyncMap) asyncMapEvent.result(); asyncMap.deleteValueByKeyPath(keyPath).setHandler(deleteResult -> { lock.release(); timerMap.remove(keyPath); }); }); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy