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

reactor.core.publisher.MonoCacheTime Maven / Gradle / Ivy

Go to download

Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC

There is a newer version: 3.40.2
Show newest version
/*
 * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved.
 *
 * 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
 *
 *   https://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 reactor.core.publisher;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.Function;
import java.util.function.Supplier;

import org.reactivestreams.Subscription;

import reactor.core.CoreSubscriber;
import reactor.core.Exceptions;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.Logger;
import reactor.util.Loggers;
import reactor.util.annotation.Nullable;
import reactor.util.context.Context;
import reactor.util.context.ContextView;

/**
 * An operator that caches the value from a source Mono with a TTL, after which the value
 * expires and the next subscription will trigger a new source subscription.
 *
 * @author Simon Baslé
 */
class MonoCacheTime extends InternalMonoOperator implements Runnable {

	private static final Duration DURATION_INFINITE = Duration.ofMillis(Long.MAX_VALUE);

	private static final Logger LOGGER = Loggers.getLogger(MonoCacheTime.class);

	final Function, Duration> ttlGenerator;
	final Scheduler                             clock;

	volatile Signal state;
	static final AtomicReferenceFieldUpdater STATE =
			AtomicReferenceFieldUpdater.newUpdater(MonoCacheTime.class, Signal.class, "state");

	static final Signal EMPTY = new ImmutableSignal<>(Context.empty(), SignalType.ON_NEXT, null, null, null);

	MonoCacheTime(Mono source, Duration ttl, Scheduler clock) {
		super(source);
		Objects.requireNonNull(ttl, "ttl must not be null");
		Objects.requireNonNull(clock, "clock must not be null");

		this.ttlGenerator = ignoredSignal -> ttl;
		this.clock = clock;
		@SuppressWarnings("unchecked")
		Signal state = (Signal) EMPTY;
		this.state = state;
	}

	MonoCacheTime(Mono source) {
		this(source, sig -> DURATION_INFINITE, Schedulers.immediate());
	}

	MonoCacheTime(Mono source, Function, Duration> ttlGenerator,
			Scheduler clock) {
		super(source);
		this.ttlGenerator = ttlGenerator;
		this.clock = clock;
		@SuppressWarnings("unchecked")
		Signal state = (Signal) EMPTY;
		this.state = state;
	}

	MonoCacheTime(Mono source,
			Function valueTtlGenerator,
			Function errorTtlGenerator,
			Supplier emptyTtlGenerator,
			Scheduler clock) {
		super(source);
		Objects.requireNonNull(valueTtlGenerator, "valueTtlGenerator must not be null");
		Objects.requireNonNull(errorTtlGenerator, "errorTtlGenerator must not be null");
		Objects.requireNonNull(emptyTtlGenerator, "emptyTtlGenerator must not be null");
		Objects.requireNonNull(clock, "clock must not be null");

		this.ttlGenerator = sig -> {
			if (sig.isOnNext()) return valueTtlGenerator.apply(sig.get());
			if (sig.isOnError()) return errorTtlGenerator.apply(sig.getThrowable());
			return emptyTtlGenerator.get();
		};
		this.clock = clock;
		@SuppressWarnings("unchecked")
		Signal emptyState = (Signal) EMPTY;
		this.state = emptyState;
	}

	public void run() {
		LOGGER.debug("expired {}", state);
		@SuppressWarnings("unchecked")
		Signal emptyState = (Signal) EMPTY;
		state = emptyState;
	}

	@Override
	public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) {
		CacheMonoSubscriber inner = new CacheMonoSubscriber<>(actual);
		actual.onSubscribe(inner);
		for(;;){
			Signal state = this.state;
			if (state == EMPTY || state instanceof CoordinatorSubscriber) {
				boolean subscribe = false;
				CoordinatorSubscriber coordinator;
				if (state == EMPTY) {
					coordinator = new CoordinatorSubscriber<>(this);
					if (!STATE.compareAndSet(this, EMPTY, coordinator)) {
						continue;
					}
					subscribe = true;
				}
				else {
					coordinator = (CoordinatorSubscriber) state;
				}

				if (coordinator.add(inner)) {
					if (inner.isCancelled()) {
						coordinator.remove(inner);
					}
					else {
						inner.coordinator = coordinator;
					}

					if (subscribe) {
						source.subscribe(coordinator);
					}
					break;
				}
			}
			else {
				//state is an actual signal, cached
				if (state.isOnNext()) {
					inner.complete(state.get());
				}
				else if (state.isOnComplete()) {
					inner.onComplete();
				}
				else {
					inner.onError(state.getThrowable());
				}
				break;
			}
		}
		return null;
	}

	@Override
	public Object scanUnsafe(Attr key) {
		if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC;
		return super.scanUnsafe(key);
	}

	static final class CoordinatorSubscriber implements InnerConsumer, Signal {

		final MonoCacheTime main;

		volatile Subscription subscription;
		static final AtomicReferenceFieldUpdater S =
				AtomicReferenceFieldUpdater.newUpdater(CoordinatorSubscriber.class, Subscription.class, "subscription");

		volatile Operators.MonoSubscriber[] subscribers;
		static final AtomicReferenceFieldUpdater SUBSCRIBERS =
				AtomicReferenceFieldUpdater.newUpdater(CoordinatorSubscriber.class, Operators.MonoSubscriber[].class, "subscribers");

		@SuppressWarnings("unchecked")
		CoordinatorSubscriber(MonoCacheTime main) {
			this.main = main;
			this.subscribers = EMPTY;
		}

		/**
		 * unused in this context as the {@link Signal} interface is only
		 * implemented for use in the main's STATE compareAndSet.
		 */
		@Override
		public Throwable getThrowable() {
			throw new UnsupportedOperationException("illegal signal use");
		}

		/**
		 * unused in this context as the {@link Signal} interface is only
		 * implemented for use in the main's STATE compareAndSet.
		 */
		@Override
		public Subscription getSubscription() {
			throw new UnsupportedOperationException("illegal signal use");
		}

		/**
		 * unused in this context as the {@link Signal} interface is only
		 * implemented for use in the main's STATE compareAndSet.
		 */
		@Override
		public T get() {
			throw new UnsupportedOperationException("illegal signal use");
		}

		/**
		 * unused in this context as the {@link Signal} interface is only
		 * implemented for use in the main's STATE compareAndSet.
		 */
		@Override
		public SignalType getType() {
			throw new UnsupportedOperationException("illegal signal use");
		}

		/**
		 * unused in this context as the {@link Signal} interface is only
		 * implemented for use in the main's STATE compareAndSet.
		 */
		@Override
		public ContextView getContextView() {
			throw new UnsupportedOperationException("illegal signal use: getContextView");
		}

		final boolean add(Operators.MonoSubscriber toAdd) {
			for (; ; ) {
				Operators.MonoSubscriber[] a = subscribers;
				if (a == TERMINATED) {
					return false;
				}
				int n = a.length;
				@SuppressWarnings("unchecked")
				Operators.MonoSubscriber[] b = new Operators.MonoSubscriber[n + 1];
				System.arraycopy(a, 0, b, 0, n);
				b[n] = toAdd;
				if (SUBSCRIBERS.compareAndSet(this, a, b)) {
					return true;
				}
			}
		}

		final void remove(Operators.MonoSubscriber toRemove) {
			for (; ; ) {
				Operators.MonoSubscriber[] a = subscribers;
				if (a == TERMINATED || a == EMPTY) {
					return;
				}
				int n = a.length;
				int j = -1;
				for (int i = 0; i < n; i++) {
					if (a[i] == toRemove) {
						j = i;
						break;
					}
				}

				if (j < 0) {
					return;
				}

				Operators.MonoSubscriber[] b;
				if (n == 1) {
					b = EMPTY;
				}
				else {
					b = new Operators.MonoSubscriber[n - 1];
					System.arraycopy(a, 0, b, 0, j);
					System.arraycopy(a, j + 1, b, j, n - j - 1);
				}
				if (SUBSCRIBERS.compareAndSet(this, a, b)) {
					//no particular cleanup here for the EMPTY case, we don't cancel the
					// source because a new subscriber could come in before the coordinator
					// is terminated.

					return;
				}
			}
		}

		@Override
		public void onSubscribe(Subscription s) {
			if (Operators.validate(subscription, s)) {
				subscription = s;
				s.request(Long.MAX_VALUE);
			}
		}

		@SuppressWarnings("unchecked")
		private void signalCached(Signal signal) {
			Signal signalToPropagate = signal;
			if (STATE.compareAndSet(main, this, signal)) {
				Duration ttl = null;
				try {
					ttl = main.ttlGenerator.apply(signal);
				}
				catch (Throwable generatorError) {
					signalToPropagate = Signal.error(generatorError);
					STATE.set(main, signalToPropagate);
					if (signal.isOnError()) {
						//noinspection ThrowableNotThrown
						Exceptions.addSuppressed(generatorError, signal.getThrowable());
					}
				}

				if (ttl != null) {
					if (ttl.isZero()) {
						//immediate cache clear
						main.run();
					}
					else if (!ttl.equals(DURATION_INFINITE)) {
						main.clock.schedule(main, ttl.toNanos(), TimeUnit.NANOSECONDS);
					}
					//else TTL is Long.MAX_VALUE, schedule nothing but still cache
				}
				else {
					//error during TTL generation, signal != updatedSignal, aka dropped
					if (signal.isOnNext()) {
						Operators.onNextDropped(signal.get(), currentContext());
					}
					//if signal.isOnError(), avoid dropping the error. it is not really dropped but already suppressed
					//in all cases, unless nextDropped hook throws, immediate cache clear
					main.run();
				}
			}

			for (Operators.MonoSubscriber inner : SUBSCRIBERS.getAndSet(this, TERMINATED)) {
				if (signalToPropagate.isOnNext()) {
					inner.complete(signalToPropagate.get());
				}
				else if (signalToPropagate.isOnError()) {
					inner.onError(signalToPropagate.getThrowable());
				}
				else {
					inner.onComplete();
				}
			}
		}

		@Override
		public void onNext(T t) {
			if (main.state != this) {
				Operators.onNextDroppedMulticast(t, subscribers);
				return;
			}
			signalCached(Signal.next(t));
		}

		@Override
		public void onError(Throwable t) {
			if (main.state != this) {
				Operators.onErrorDroppedMulticast(t, subscribers);
				return;
			}
			signalCached(Signal.error(t));
		}

		@Override
		public void onComplete() {
			signalCached(Signal.complete());
		}

		@Override
		public Context currentContext() {
			return Operators.multiSubscribersContext(subscribers);
		}

		@Nullable
		@Override
		public Object scanUnsafe(Attr key) {
			if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC;
			return null;
		}

		private static final Operators.MonoSubscriber[] TERMINATED        = new Operators.MonoSubscriber[0];
		private static final Operators.MonoSubscriber[] EMPTY             = new Operators.MonoSubscriber[0];
	}

	static final class CacheMonoSubscriber extends Operators.MonoSubscriber {

		CoordinatorSubscriber coordinator;

		CacheMonoSubscriber(CoreSubscriber actual) {
			super(actual);
		}

		@Override
		public void cancel() {
			super.cancel();
			CoordinatorSubscriber coordinator = this.coordinator;
			if (coordinator != null) {
				coordinator.remove(this);
			}
		}

		@Override
		public Object scanUnsafe(Attr key) {
			if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC;
			return super.scanUnsafe(key);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy