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

com.hazelcast.jet.impl.processor.AsyncTransformUsingServiceOrderedP Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2008-2024, Hazelcast, Inc. 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
 *
 * 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 com.hazelcast.jet.impl.processor;

import com.hazelcast.function.BiFunctionEx;
import com.hazelcast.internal.metrics.Probe;
import com.hazelcast.internal.util.counters.Counter;
import com.hazelcast.internal.util.counters.SwCounter;
import com.hazelcast.jet.JetException;
import com.hazelcast.jet.Traverser;
import com.hazelcast.jet.Traversers;
import com.hazelcast.jet.core.ProcessorSupplier;
import com.hazelcast.jet.core.ResettableSingletonTraverser;
import com.hazelcast.jet.core.Watermark;
import com.hazelcast.jet.datamodel.Tuple2;
import com.hazelcast.jet.pipeline.ServiceFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.concurrent.CompletableFuture;

import static com.hazelcast.jet.datamodel.Tuple2.tuple2;
import static com.hazelcast.jet.impl.processor.ProcessorSupplierWithService.supplierWithService;

/**
 * Processor which, for each received item, emits all the items from the
 * traverser returned by the given async item-to-traverser function, using a
 * service.
 * 

* This processor keeps the order of input items: a stalling call for one item * will stall all subsequent items. * * @param context object type * @param received item type * @param intermediate result type * @param emitted item type */ public class AsyncTransformUsingServiceOrderedP extends AbstractAsyncTransformUsingServiceP { private final BiFunctionEx> callAsyncFn; private final BiFunctionEx> mapResultFn; // The queue holds both watermarks and output items private ArrayDeque queue; // The number of watermarks in the queue private int queuedWmCount; private Traverser currentTraverser = Traversers.empty(); private final ResettableSingletonTraverser watermarkTraverser = new ResettableSingletonTraverser<>(); @Probe(name = "numInFlightOps") private final Counter asyncOpsCounterMetric = SwCounter.newSwCounter(); /** * Constructs a processor with the given mapping function. * * @param mapResultFn a function to map the intermediate result returned by * the future. One could think it's the same as CompletableFuture.thenApply(), * however, the {@code mapResultFn} is executed on the processor thread * and not concurrently, therefore the function can be stateful. */ public AsyncTransformUsingServiceOrderedP( @Nonnull ServiceFactory serviceFactory, @Nullable C serviceContext, int maxConcurrentOps, @Nonnull BiFunctionEx> callAsyncFn, @Nonnull BiFunctionEx> mapResultFn ) { super(serviceFactory, serviceContext, maxConcurrentOps, true); this.callAsyncFn = callAsyncFn; this.mapResultFn = mapResultFn; } @Override protected void init(@Nonnull Context context) throws Exception { super.init(context); // Size for the worst case: interleaved output items an WMs queue = new ArrayDeque<>(maxConcurrentOps * 2); } @Override @SuppressWarnings("unchecked") protected boolean tryProcess(int ordinal, @Nonnull Object item) { if (makeRoomInQueue()) { return tryProcessInt((T) item); } return false; } protected boolean tryProcessInt(T item) { CompletableFuture future = callAsyncFn.apply(service, item); if (future != null) { queue.add(tuple2(item, future)); } return true; } /** * If the queue is full, try to flush some items. Return true, if there's * some space in the queue after this call. */ protected boolean makeRoomInQueue() { if (isQueueFull()) { tryFlushQueue(); return !isQueueFull(); } return true; } boolean isQueueFull() { return queue.size() - queuedWmCount == maxConcurrentOps; } @Override public boolean tryProcessWatermark(@Nonnull Watermark watermark) { Object lastItem = queue.peekLast(); if (lastItem instanceof Watermark lastWatermark && watermark.key() == lastWatermark.key()) { // conflate the previous wm with the current one queue.removeLast(); queue.add(watermark); } else { queue.add(watermark); queuedWmCount++; } return true; } @Override public boolean tryProcess() { tryFlushQueue(); asyncOpsCounterMetric.set(queue.size()); return true; } @Override public boolean complete() { return tryFlushQueue(); } @Override public boolean saveToSnapshot() { // We're stateless, wait until responses to all async requests are emitted. This is a // stop-the-world situation, no new async requests are sent while waiting. If async requests // are slow, this might be a major slowdown. return tryFlushQueue(); } /** * Drains items from the queue until either: *
  • * encountering a non-completed item *
  • * the outbox gets full *
* * @return true if there are no more in-flight items and everything was emitted * to the outbox */ boolean tryFlushQueue() { // We check the futures in submission order. While this might increase latency for some // later-submitted item that gets the result before some earlier-submitted one, we don't // have to do many volatile reads to check all the futures in each call or a concurrent // queue. It also doesn't shuffle the stream items. for (;;) { if (!emitFromTraverser(currentTraverser)) { return false; } Object o = queue.peek(); if (o == null) { return true; } if (o instanceof Watermark watermark) { watermarkTraverser.accept(watermark); currentTraverser = watermarkTraverser; queuedWmCount--; } else { @SuppressWarnings("unchecked") Tuple2> cast = (Tuple2>) o; T item = cast.f0(); CompletableFuture future = cast.f1(); assert future != null; if (!future.isDone()) { return false; } try { currentTraverser = mapResultFn.apply(item, future.get()); if (currentTraverser == null) { currentTraverser = Traversers.empty(); } } catch (Throwable e) { throw new JetException("Async operation completed exceptionally: " + e, e); } } queue.remove(); } } /** * The {@link ResettableSingletonTraverser} is passed as a first argument to * {@code callAsyncFn}, it can be used if needed. */ public static ProcessorSupplier supplier( @Nonnull ServiceFactory serviceFactory, int maxConcurrentOps, @Nonnull BiFunctionEx>> callAsyncFn ) { return supplierWithService(serviceFactory, (serviceFn, context) -> new AsyncTransformUsingServiceOrderedP<>(serviceFn, context, maxConcurrentOps, callAsyncFn, (i, r) -> r)); } }