package org.infinispan.server.hotrod; import javax.security.auth.Subject; import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.container.entries.CacheEntry; import org.infinispan.container.versioning.EntryVersion; import org.infinispan.container.versioning.NumericVersion; import org.infinispan.container.versioning.NumericVersionGenerator; import org.infinispan.container.versioning.SimpleClusteredVersion; import org.infinispan.container.versioning.VersionGenerator; import org.infinispan.context.Flag; import org.infinispan.factories.ComponentRegistry; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.metadata.EmbeddedMetadata; import org.infinispan.metadata.Metadata; import org.infinispan.registry.InternalCacheRegistry; import org.infinispan.remoting.rpc.RpcManager; import org.infinispan.server.core.ServerConstants; import org.infinispan.server.hotrod.logging.Log; import org.infinispan.util.logging.LogFactory; /** * Invokes operations against the cache based on the state kept during decoding process */ public final class CacheDecodeContext { static final long MillisecondsIn30days = 60 * 60 * 24 * 30 * 1000l; static final Log log = LogFactory.getLog(CacheDecodeContext.class, Log.class); static final boolean isTrace = log.isTraceEnabled(); private final HotRodServer server; CacheDecodeContext(HotRodServer server) { this.server = server; } VersionedDecoder decoder; HotRodHeader header; Subject subject; AdvancedCache<byte[], byte[]> cache; byte[] key; RequestParameters params; Object operationDecodeContext; public HotRodHeader getHeader() { return header; } public byte[] getKey() { return key; } public RequestParameters getParams() { return params; } ErrorResponse createExceptionResponse(Throwable e) { if (e instanceof InvalidMagicIdException) { log.exceptionReported(e); return new ErrorResponse((byte) 0, 0, "", (short) 1, OperationStatus.InvalidMagicOrMsgId, 0, e.toString()); } else if (e instanceof HotRodUnknownOperationException) { log.exceptionReported(e); HotRodUnknownOperationException hruoe = (HotRodUnknownOperationException) e; return new ErrorResponse(hruoe.version, hruoe.messageId, "", (short) 1, OperationStatus.UnknownOperation, 0, e.toString()); } else if (e instanceof UnknownVersionException) { log.exceptionReported(e); UnknownVersionException uve = (UnknownVersionException) e; return new ErrorResponse(uve.version, uve.messageId, "", (short) 1, OperationStatus.UnknownVersion, 0, e.toString()); } else if (e instanceof RequestParsingException) { log.exceptionReported(e); String msg = e.getCause() == null ? e.toString() : String.format("%s: %s", e.getMessage(), e.getCause().toString()); RequestParsingException rpe = (RequestParsingException) e; return new ErrorResponse(rpe.version, rpe.messageId, "", (short) 1, OperationStatus.ParseError, 0, msg); } else if (e instanceof IllegalStateException) { // Some internal server code could throw this, so make sure it's logged log.exceptionReported(e); return decoder.createErrorResponse(header, e); } else if (decoder != null) { return decoder.createErrorResponse(header, e); } else { log.exceptionReported(e); return new ErrorResponse((byte) 0, 0, "", (short) 1, OperationStatus.ServerError, 1, e.toString()); } } Response replace() { // Avoid listener notification for a simple optimization // on whether a new version should be calculated or not. byte[] prev = cache.withFlags(Flag.SKIP_LISTENER_NOTIFICATION).get(key); if (prev != null) { // Generate new version only if key present prev = cache.replace(key, (byte[]) operationDecodeContext, buildMetadata()); } if (prev != null) return successResp(prev); else return notExecutedResp(prev); } void obtainCache(EmbeddedCacheManager cacheManager, boolean loopback) throws RequestParsingException { String cacheName = header.cacheName; // Try to avoid calling cacheManager.getCacheNames() if possible, since this creates a lot of unnecessary garbage AdvancedCache<byte[], byte[]> cache = server.getKnownCacheInstance(cacheName); if (cache == null) { // Talking to the wrong cache are really request parsing errors // and hence should be treated as client errors InternalCacheRegistry icr = cacheManager.getGlobalComponentRegistry().getComponent(InternalCacheRegistry.class); if (icr.isPrivateCache(cacheName)) { throw new RequestParsingException( String.format("Remote requests are not allowed to private caches. Do no send remote requests to cache '%s'", cacheName), header.version, header.messageId); } else if (icr.internalCacheHasFlag(cacheName, InternalCacheRegistry.Flag.PROTECTED)) { if (!cacheManager.getCacheManagerConfiguration().security().authorization().enabled() && !loopback) { throw new RequestParsingException( String.format("Remote requests are allowed to protected caches only over loopback or if authorization is enabled. Do no send remote requests to cache '%s'", cacheName), header.version, header.messageId); } else { // We want to make sure the cache access is checked everytime, so don't store it as a "known" cache. More // expensive, but these caches should not be accessed frequently cache = server.getCacheInstance(cacheName, cacheManager, true, false); } } else if (!cacheName.isEmpty() && !cacheManager.getCacheNames().contains(cacheName)) { throw new CacheNotFoundException( String.format("Cache with name '%s' not found amongst the configured caches", cacheName), header.version, header.messageId); } else { cache = server.getCacheInstance(cacheName, cacheManager, true, true); } } this.cache = decoder.getOptimizedCache(header, cache, server.getCacheConfiguration(cacheName)); } Metadata buildMetadata() { EmbeddedMetadata.Builder metadata = new EmbeddedMetadata.Builder(); metadata.version(generateVersion(server.getCacheRegistry(header.cacheName), cache)); if (params.lifespan.duration != ServerConstants.EXPIRATION_DEFAULT) { metadata.lifespan(toMillis(params.lifespan, header)); } if (params.maxIdle.duration != ServerConstants.EXPIRATION_DEFAULT) { metadata.maxIdle(toMillis(params.maxIdle, header)); } return metadata.build(); } Response get() { return createGetResponse(cache.getCacheEntry(key)); } Response getKeyMetadata() { CacheEntry<byte[], byte[]> ce = cache.getCacheEntry(key); if (ce != null) { EntryVersion entryVersion = ce.getMetadata().version(); long version = extractVersion(entryVersion); byte[] v = ce.getValue(); int lifespan = ce.getLifespan() < 0 ? -1 : (int) ce.getLifespan() / 1000; int maxIdle = ce.getMaxIdle() < 0 ? -1 : (int) ce.getMaxIdle() / 1000; if (header.op == HotRodOperation.GET_WITH_METADATA) { return new GetWithMetadataResponse(header.version, header.messageId, header.cacheName, header.clientIntel, header.op, OperationStatus.Success, header.topologyId, v, version, ce.getCreated(), lifespan, ce.getLastUsed(), maxIdle); } else { int offset = ((Integer) operationDecodeContext).intValue(); return new GetStreamResponse(header.version, header.messageId, header.cacheName, header.clientIntel, header.op, OperationStatus.Success, header.topologyId, v, offset, version, ce.getCreated(), lifespan, ce.getLastUsed(), maxIdle); } } else { if (header.op == HotRodOperation.GET_WITH_METADATA) { return new GetWithMetadataResponse(header.version, header.messageId, header.cacheName, header.clientIntel, header.op, OperationStatus.KeyDoesNotExist, header.topologyId); } else { return new GetStreamResponse(header.version, header.messageId, header.cacheName, header.clientIntel, header.op, OperationStatus.KeyDoesNotExist, header.topologyId); } } } static long extractVersion(EntryVersion entryVersion) { long version = 0; if (entryVersion != null) { if (entryVersion instanceof NumericVersion) { version = NumericVersion.class.cast(entryVersion).getVersion(); } if (entryVersion instanceof SimpleClusteredVersion) { version = SimpleClusteredVersion.class.cast(entryVersion).getVersion(); } } return version; } Response containsKey() { if (cache.containsKey(key)) return successResp(null); else return notExistResp(); } Response replaceIfUnmodified() { CacheEntry<byte[], byte[]> entry = cache.withFlags(Flag.SKIP_LISTENER_NOTIFICATION).getCacheEntry(key); if (entry != null) { byte[] prev = entry.getValue(); NumericVersion streamVersion = new NumericVersion(params.streamVersion); if (entry.getMetadata().version().equals(streamVersion)) { // Generate new version only if key present and version has not changed, otherwise it's wasteful boolean replaced = cache.replace(key, prev, (byte[]) operationDecodeContext, buildMetadata()); if (replaced) return successResp(prev); else return notExecutedResp(prev); } else { return notExecutedResp(prev); } } else return notExistResp(); } Response putIfAbsent() { byte[] prev = cache.get(key); if (prev == null) { // Generate new version only if key not present prev = cache.putIfAbsent(key, (byte[]) operationDecodeContext, buildMetadata()); } if (prev == null) return successResp(prev); else return notExecutedResp(prev); } Response put() { // Get an optimised cache in case we can make the operation more efficient byte[] prev = cache.put(key, (byte[]) operationDecodeContext, buildMetadata()); return successResp(prev); } EntryVersion generateVersion(ComponentRegistry registry, Cache<byte[], byte[]> cache) { VersionGenerator cacheVersionGenerator = registry.getVersionGenerator(); if (cacheVersionGenerator == null) { // It could be null, for example when not running in compatibility mode. // The reason for that is that if no other component depends on the // version generator, the factory does not get invoked. NumericVersionGenerator newVersionGenerator = new NumericVersionGenerator() .clustered(registry.getComponent(RpcManager.class) != null); registry.registerComponent(newVersionGenerator, VersionGenerator.class); return newVersionGenerator.generateNew(); } else { return cacheVersionGenerator.generateNew(); } } Response remove() { byte[] prev = cache.remove(key); if (prev != null) return successResp(prev); else return notExistResp(); } Response removeIfUnmodified() { CacheEntry<byte[], byte[]> entry = cache.getCacheEntry(key); if (entry != null) { byte[] prev = entry.getValue(); NumericVersion streamVersion = new NumericVersion(params.streamVersion); if (entry.getMetadata().version().equals(streamVersion)) { boolean removed = cache.remove(key, prev); if (removed) return successResp(prev); else return notExecutedResp(prev); } else { return notExecutedResp(prev); } } else { return notExistResp(); } } Response clear() { cache.clear(); return successResp(null); } Response successResp(byte[] prev) { return decoder.createSuccessResponse(header, prev); } Response notExecutedResp(byte[] prev) { return decoder.createNotExecutedResponse(header, prev); } Response notExistResp() { return decoder.createNotExistResponse(header); } Response createGetResponse(CacheEntry<byte[], byte[]> entry) { return decoder.createGetResponse(header, entry); } ComponentRegistry getCacheRegistry(String cacheName) { return server.getCacheRegistry(cacheName); } static class ExpirationParam { final long duration; final TimeUnitValue unit; ExpirationParam(long duration, TimeUnitValue unit) { this.duration = duration; this.unit = unit; } @Override public String toString() { return "ExpirationParam{duration=" + duration + ", unit=" + unit + '}'; } } static class RequestParameters { final int valueLength; final ExpirationParam lifespan; final ExpirationParam maxIdle; final long streamVersion; RequestParameters(int valueLength, ExpirationParam lifespan, ExpirationParam maxIdle, long streamVersion) { this.valueLength = valueLength; this.lifespan = lifespan; this.maxIdle = maxIdle; this.streamVersion = streamVersion; } @Override public String toString() { return "RequestParameters{" + "valueLength=" + valueLength + ", lifespan=" + lifespan + ", maxIdle=" + maxIdle + ", streamVersion=" + streamVersion + '}'; } } /** * Transforms lifespan pass as seconds into milliseconds following this rule (inspired by Memcached): * <p> * If lifespan is bigger than number of seconds in 30 days, then it is considered unix time. After converting it to * milliseconds, we subtract the current time in and the result is returned. * <p> * Otherwise it's just considered number of seconds from now and it's returned in milliseconds unit. */ static long toMillis(ExpirationParam param, HotRodHeader h) { if (param.duration > 0) { long milliseconds = param.unit.toTimeUnit().toMillis(param.duration); if (milliseconds > MillisecondsIn30days) { long unixTimeExpiry = milliseconds - System.currentTimeMillis(); return unixTimeExpiry < 0 ? 0 : unixTimeExpiry; } else { return milliseconds; } } else { return param.duration; } } }