001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.input; 017 018import static java.lang.Thread.sleep; 019import static java.util.concurrent.TimeUnit.MILLISECONDS; 020import static org.talend.sdk.component.runtime.input.Streaming.RetryStrategy; 021 022import java.io.IOException; 023import java.io.InvalidObjectException; 024import java.io.ObjectStreamException; 025import java.io.Serializable; 026import java.lang.annotation.Annotation; 027import java.lang.reflect.Method; 028import java.lang.reflect.Parameter; 029import java.util.concurrent.ExecutorService; 030import java.util.concurrent.Executors; 031import java.util.concurrent.Future; 032import java.util.concurrent.Semaphore; 033import java.util.concurrent.TimeoutException; 034import java.util.concurrent.atomic.AtomicBoolean; 035 036import javax.annotation.PostConstruct; 037 038import org.talend.sdk.component.api.configuration.Option; 039import org.talend.sdk.component.runtime.input.Streaming.RetryConfiguration; 040import org.talend.sdk.component.runtime.input.Streaming.StopStrategy; 041 042import lombok.extern.slf4j.Slf4j; 043 044@Slf4j 045public class StreamingInputImpl extends InputImpl { 046 047 private RetryConfiguration retryConfiguration; 048 049 private transient Thread shutdownHook; 050 051 private final AtomicBoolean running = new AtomicBoolean(); 052 053 private transient Semaphore semaphore; 054 055 private StopStrategy stopStrategy; 056 057 private transient long readRecords = 0L; 058 059 public StreamingInputImpl(final String rootName, final String name, final String plugin, 060 final Serializable instance, final RetryConfiguration retryConfiguration, final StopStrategy stopStrategy) { 061 super(rootName, name, plugin, instance); 062 shutdownHook = new Thread(() -> running.compareAndSet(true, false), 063 getClass().getName() + "_" + rootName() + "-" + name() + "_" + hashCode()); 064 this.retryConfiguration = retryConfiguration; 065 this.stopStrategy = stopStrategy; 066 log.debug("[StreamingInputImpl] Created with retryStrategy: {}, stopStrategy: {}.", this.retryConfiguration, 067 this.stopStrategy); 068 } 069 070 protected StreamingInputImpl() { 071 // no-op 072 } 073 074 @Override 075 protected Object readNext() { 076 if (!running.get()) { 077 return null; 078 } 079 if (stopStrategy.isActive() && stopStrategy.shouldStop(readRecords)) { 080 log.debug("[readNext] stopStrategy condition validated."); 081 return null; 082 } 083 try { 084 semaphore.acquire(); 085 } catch (final InterruptedException e) { 086 Thread.currentThread().interrupt(); 087 return null; 088 } 089 try { 090 final RetryStrategy strategy = retryConfiguration.getStrategy(); 091 int retries = retryConfiguration.getMaxRetries(); 092 while (running.get() && retries > 0) { 093 Object next = null; 094 if (stopStrategy.isActive() && stopStrategy.getMaxActiveTime() > -1) { 095 // Some connectors do not block input and return null (rabbitmq for instance). Thus, the future 096 // timeout is never reached and retryStrategy is run then. So, need to check timeout in the loop. 097 if (stopStrategy.shouldStop(readRecords)) { 098 log.debug("[readNext] shouldStop now! Duration {}ms", 099 System.currentTimeMillis() - stopStrategy.getStartedAtTime()); 100 return null; 101 } 102 final ExecutorService executor = Executors.newSingleThreadExecutor(); 103 final Future<Object> reader = executor.submit(super::readNext); 104 // manage job latency... 105 // timeout + hardcoded grace period 106 final long maxActiveTimeWithGracePeriod = 107 stopStrategy.getMaxActiveTime() + Streaming.MAX_DURATION_TIME_MS_GRACE_PERIOD; 108 final long estimatedTimeout = maxActiveTimeWithGracePeriod 109 - (System.currentTimeMillis() - stopStrategy.getStartedAtTime()); 110 final long timeout = estimatedTimeout < -1 ? 10 : estimatedTimeout; 111 log.debug( 112 "[readNext] Applying duration strategy for reading record: will interrupt in {}ms (estimated:{} ms Duration:{}ms).", 113 timeout, estimatedTimeout, maxActiveTimeWithGracePeriod); 114 try { 115 next = reader.get(timeout, MILLISECONDS); 116 } catch (TimeoutException e) { 117 log.debug("[readNext] Read record: timeout received."); 118 reader.cancel(true); 119 return next; 120 } catch (Exception e) { 121 // nop 122 } finally { 123 executor.shutdownNow(); 124 } 125 } else { 126 next = super.readNext(); 127 } 128 if (next != null) { 129 strategy.reset(); 130 readRecords++; 131 return next; 132 } 133 134 retries--; 135 try { 136 final long millis = strategy.nextPauseDuration(); 137 if (millis < 0) { // assume it means "give up" 138 prepareStop(); 139 } else if (millis > 0) { // we can wait 1s but not minutes to quit 140 if (millis < 1000) { 141 sleep(millis); 142 } else { 143 long remaining = millis; 144 while (running.get() && remaining > 0) { 145 final long current = Math.min(remaining, 250); 146 remaining -= current; 147 sleep(current); 148 } 149 } 150 } // else if millis == 0 no need to call any method 151 } catch (final InterruptedException e) { 152 prepareStop(); // stop the stream 153 } 154 } 155 return null; 156 } finally { 157 semaphore.release(); 158 } 159 } 160 161 @Override 162 protected void init() { 163 super.init(); 164 semaphore = new Semaphore(1); 165 } 166 167 @Override 168 protected Object[] evaluateParameters(final Class<? extends Annotation> marker, final Method method) { 169 if (marker != PostConstruct.class) { 170 return super.evaluateParameters(marker, method); 171 } 172 173 final Object[] args = new Object[method.getParameters().length]; 174 for (int i = 0; i < method.getParameters().length; i++) { 175 final Parameter parameter = method.getParameters()[i]; 176 177 final Option annotation = parameter.getAnnotation(Option.class); 178 if (annotation == null) { 179 args[i] = null; 180 } else if (Option.MAX_DURATION_PARAMETER.equals(annotation.value())) { 181 if (parameter.getType() == Integer.class || parameter.getType() == int.class) { 182 args[i] = (int) stopStrategy.getMaxActiveTime(); 183 } else if (parameter.getType() == Long.class || parameter.getType() == long.class) { 184 args[i] = stopStrategy.getMaxActiveTime(); 185 } else { 186 log.warn("The parameter {} is marked as timeout but type is not long.", parameter.getName()); 187 args[i] = null; 188 } 189 } else if (Option.MAX_RECORDS_PARAMETER.equals(annotation.value())) { 190 if (parameter.getType() == Integer.class || parameter.getType() == int.class) { 191 args[i] = (int) stopStrategy.getMaxReadRecords(); 192 } else if (parameter.getType() == Long.class || parameter.getType() == long.class) { 193 args[i] = stopStrategy.getMaxReadRecords(); 194 } else { 195 log.warn("The parameter {} is marked as max records limitation but type is not long.", 196 parameter.getName()); 197 args[i] = null; 198 } 199 200 } 201 } 202 return args; 203 } 204 205 @Override 206 public void start() { 207 super.start(); 208 running.compareAndSet(false, true); 209 Runtime.getRuntime().addShutdownHook(shutdownHook); 210 } 211 212 @Override 213 public void stop() { 214 prepareStop(); 215 super.stop(); 216 } 217 218 private void prepareStop() { 219 running.compareAndSet(true, false); 220 if (shutdownHook != null) { 221 try { 222 Runtime.getRuntime().removeShutdownHook(shutdownHook); 223 } catch (final IllegalStateException itse) { 224 // ok to ignore 225 } 226 } 227 try { 228 semaphore.acquire(); 229 } catch (final InterruptedException e) { 230 Thread.currentThread().interrupt(); 231 } 232 } 233 234 @Override 235 protected Object writeReplace() throws ObjectStreamException { 236 return new StreamSerializationReplacer(plugin(), rootName(), name(), serializeDelegate(), retryConfiguration, 237 stopStrategy); 238 } 239 240 private static class StreamSerializationReplacer extends SerializationReplacer { 241 242 private final RetryConfiguration retryConfiguration; 243 244 private final StopStrategy stopStrategy; 245 246 StreamSerializationReplacer(final String plugin, final String component, final String name, final byte[] value, 247 final RetryConfiguration retryConfiguration, final StopStrategy stopStrategy) { 248 super(plugin, component, name, value); 249 this.retryConfiguration = retryConfiguration; 250 this.stopStrategy = stopStrategy; 251 } 252 253 protected Object readResolve() throws ObjectStreamException { 254 try { 255 return new StreamingInputImpl(component, name, plugin, loadDelegate(), retryConfiguration, 256 stopStrategy); 257 } catch (final IOException | ClassNotFoundException e) { 258 final InvalidObjectException invalidObjectException = new InvalidObjectException(e.getMessage()); 259 invalidObjectException.initCause(e); 260 throw invalidObjectException; 261 } 262 } 263 } 264}