001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.impl;
018
019import java.io.File;
020import java.lang.management.ManagementFactory;
021import java.lang.management.MemoryMXBean;
022import java.util.LinkedHashSet;
023import java.util.Set;
024import java.util.UUID;
025
026import org.apache.camel.CamelContext;
027import org.apache.camel.CamelContextAware;
028import org.apache.camel.Exchange;
029import org.apache.camel.Message;
030import org.apache.camel.StreamCache;
031import org.apache.camel.spi.StreamCachingStrategy;
032import org.apache.camel.util.FilePathResolver;
033import org.apache.camel.util.FileUtil;
034import org.apache.camel.util.IOHelper;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Default implementation of {@link StreamCachingStrategy}
040 */
041public class DefaultStreamCachingStrategy extends org.apache.camel.support.ServiceSupport implements CamelContextAware, StreamCachingStrategy {
042
043    @Deprecated
044    public static final String THRESHOLD = "CamelCachedOutputStreamThreshold";
045    @Deprecated
046    public static final String BUFFER_SIZE = "CamelCachedOutputStreamBufferSize";
047    @Deprecated
048    public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory";
049    @Deprecated
050    public static final String CIPHER_TRANSFORMATION = "CamelCachedOutputStreamCipherTransformation";
051
052    private static final Logger LOG = LoggerFactory.getLogger(DefaultStreamCachingStrategy.class);
053
054    private CamelContext camelContext;
055    private boolean enabled;
056    private File spoolDirectory;
057    private transient String spoolDirectoryName = "${java.io.tmpdir}/camel/camel-tmp-#uuid#";
058    private long spoolThreshold = StreamCache.DEFAULT_SPOOL_THRESHOLD;
059    private int spoolUsedHeapMemoryThreshold;
060    private SpoolUsedHeapMemoryLimit spoolUsedHeapMemoryLimit;
061    private String spoolCihper;
062    private int bufferSize = IOHelper.DEFAULT_BUFFER_SIZE;
063    private boolean removeSpoolDirectoryWhenStopping = true;
064    private final UtilizationStatistics statistics = new UtilizationStatistics();
065    private final Set<SpoolRule> spoolRules = new LinkedHashSet<>();
066    private boolean anySpoolRules;
067
068    public CamelContext getCamelContext() {
069        return camelContext;
070    }
071
072    public void setCamelContext(CamelContext camelContext) {
073        this.camelContext = camelContext;
074    }
075
076    public boolean isEnabled() {
077        return enabled;
078    }
079
080    public void setEnabled(boolean enabled) {
081        this.enabled = enabled;
082    }
083
084    public void setSpoolDirectory(String path) {
085        this.spoolDirectoryName = path;
086    }
087
088    public void setSpoolDirectory(File path) {
089        this.spoolDirectory = path;
090    }
091
092    public File getSpoolDirectory() {
093        return spoolDirectory;
094    }
095
096    public long getSpoolThreshold() {
097        return spoolThreshold;
098    }
099
100    public int getSpoolUsedHeapMemoryThreshold() {
101        return spoolUsedHeapMemoryThreshold;
102    }
103
104    public void setSpoolUsedHeapMemoryThreshold(int spoolHeapMemoryWatermarkThreshold) {
105        this.spoolUsedHeapMemoryThreshold = spoolHeapMemoryWatermarkThreshold;
106    }
107
108    public SpoolUsedHeapMemoryLimit getSpoolUsedHeapMemoryLimit() {
109        return spoolUsedHeapMemoryLimit;
110    }
111
112    public void setSpoolUsedHeapMemoryLimit(SpoolUsedHeapMemoryLimit spoolUsedHeapMemoryLimit) {
113        this.spoolUsedHeapMemoryLimit = spoolUsedHeapMemoryLimit;
114    }
115
116    public void setSpoolThreshold(long spoolThreshold) {
117        this.spoolThreshold = spoolThreshold;
118    }
119
120    public String getSpoolChiper() {
121        return getSpoolCipher();
122    }
123
124    public void setSpoolChiper(String spoolChiper) {
125        setSpoolCipher(spoolChiper);
126    }
127
128    public String getSpoolCipher() {
129        return spoolCihper;
130    }
131
132    public void setSpoolCipher(String spoolCipher) {
133        this.spoolCihper = spoolCipher;
134    }
135
136    public int getBufferSize() {
137        return bufferSize;
138    }
139
140    public void setBufferSize(int bufferSize) {
141        this.bufferSize = bufferSize;
142    }
143
144    public boolean isRemoveSpoolDirectoryWhenStopping() {
145        return removeSpoolDirectoryWhenStopping;
146    }
147
148    public void setRemoveSpoolDirectoryWhenStopping(boolean removeSpoolDirectoryWhenStopping) {
149        this.removeSpoolDirectoryWhenStopping = removeSpoolDirectoryWhenStopping;
150    }
151
152    public boolean isAnySpoolRules() {
153        return anySpoolRules;
154    }
155
156    public void setAnySpoolRules(boolean anySpoolTasks) {
157        this.anySpoolRules = anySpoolTasks;
158    }
159
160    public Statistics getStatistics() {
161        return statistics;
162    }
163
164    public boolean shouldSpoolCache(long length) {
165        if (!enabled || spoolRules.isEmpty()) {
166            return false;
167        }
168
169        boolean all = true;
170        boolean any = false;
171        for (SpoolRule rule : spoolRules) {
172            boolean result = rule.shouldSpoolCache(length);
173            if (!result) {
174                all = false;
175                if (!anySpoolRules) {
176                    // no need to check anymore
177                    break;
178                }
179            } else {
180                any = true;
181                if (anySpoolRules) {
182                    // no need to check anymore
183                    break;
184                }
185            }
186        }
187
188        boolean answer = anySpoolRules ? any : all;
189        LOG.debug("Should spool cache {} -> {}", length, answer);
190        return answer;
191    }
192
193    public void addSpoolRule(SpoolRule rule) {
194        spoolRules.add(rule);
195    }
196
197    public StreamCache cache(Exchange exchange) {
198        Message message = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
199        StreamCache cache = message.getBody(StreamCache.class);
200        if (cache != null) {
201            if (LOG.isTraceEnabled()) {
202                LOG.trace("Cached stream to {} -> {}", cache.inMemory() ? "memory" : "spool", cache);
203            }
204            if (statistics.isStatisticsEnabled()) {
205                try {
206                    if (cache.inMemory()) {
207                        statistics.updateMemory(cache.length());
208                    } else {
209                        statistics.updateSpool(cache.length());
210                    }
211                } catch (Exception e) {
212                    LOG.debug("Error updating cache statistics. This exception is ignored.", e);
213                }
214            }
215        }
216        return cache;
217    }
218
219    protected String resolveSpoolDirectory(String path) {
220        String name = camelContext.getManagementNameStrategy().resolveManagementName(path, camelContext.getName(), false);
221        if (name != null) {
222            name = customResolveManagementName(name);
223        }
224        // and then check again with invalid check to ensure all ## is resolved
225        if (name != null) {
226            name = camelContext.getManagementNameStrategy().resolveManagementName(name, camelContext.getName(), true);
227        }
228        return name;
229    }
230
231    protected String customResolveManagementName(String pattern) {
232        if (pattern.contains("#uuid#")) {
233            String uuid = UUID.randomUUID().toString();
234            pattern = pattern.replaceFirst("#uuid#", uuid);
235        }
236        return FilePathResolver.resolvePath(pattern);
237    }
238
239    @Override
240    protected void doStart() throws Exception {
241        if (!enabled) {
242            LOG.debug("StreamCaching is not enabled");
243            return;
244        }
245
246        String bufferSize = camelContext.getGlobalOption(BUFFER_SIZE);
247        String hold = camelContext.getGlobalOption(THRESHOLD);
248        String cipher = camelContext.getGlobalOption(CIPHER_TRANSFORMATION);
249        String dir = camelContext.getGlobalOption(TEMP_DIR);
250
251        boolean warn = false;
252        if (bufferSize != null) {
253            warn = true;
254            this.bufferSize = camelContext.getTypeConverter().convertTo(Integer.class, bufferSize);
255        }
256        if (hold != null) {
257            warn = true;
258            this.spoolThreshold = camelContext.getTypeConverter().convertTo(Long.class, hold);
259        }
260        if (cipher != null) {
261            warn = true;
262            this.spoolCihper = cipher;
263        }
264        if (dir != null) {
265            warn = true;
266            this.spoolDirectory = camelContext.getTypeConverter().convertTo(File.class, dir);
267        }
268        if (warn) {
269            LOG.warn("Configuring of StreamCaching using CamelContext properties is deprecated - use StreamCachingStrategy instead.");
270        }
271
272        if (spoolUsedHeapMemoryThreshold > 99) {
273            throw new IllegalArgumentException("SpoolHeapMemoryWatermarkThreshold must not be higher than 99, was: " + spoolUsedHeapMemoryThreshold);
274        }
275
276        // if we can overflow to disk then make sure directory exists / is created
277        if (spoolThreshold > 0 || spoolUsedHeapMemoryThreshold > 0) {
278
279            if (spoolDirectory == null && spoolDirectoryName == null) {
280                throw new IllegalArgumentException("SpoolDirectory must be configured when using SpoolThreshold > 0");
281            }
282
283            if (spoolDirectory == null) {
284                String name = resolveSpoolDirectory(spoolDirectoryName);
285                if (name != null) {
286                    spoolDirectory = new File(name);
287                    spoolDirectoryName = null;
288                } else {
289                    throw new IllegalStateException("Cannot resolve spool directory from pattern: " + spoolDirectoryName);
290                }
291            }
292
293            if (spoolDirectory.exists()) {
294                if (spoolDirectory.isDirectory()) {
295                    LOG.debug("Using spool directory: {}", spoolDirectory);
296                } else {
297                    LOG.warn("Spool directory: {} is not a directory. This may cause problems spooling to disk for the stream caching!", spoolDirectory);
298                }
299            } else {
300                boolean created = spoolDirectory.mkdirs();
301                if (!created) {
302                    LOG.warn("Cannot create spool directory: {}. This may cause problems spooling to disk for the stream caching!", spoolDirectory);
303                } else {
304                    LOG.debug("Created spool directory: {}", spoolDirectory);
305                }
306
307            }
308
309            if (spoolThreshold > 0) {
310                spoolRules.add(new FixedThresholdSpoolRule());
311            }
312            if (spoolUsedHeapMemoryThreshold > 0) {
313                if (spoolUsedHeapMemoryLimit == null) {
314                    // use max by default
315                    spoolUsedHeapMemoryLimit = SpoolUsedHeapMemoryLimit.Max;
316                }
317                spoolRules.add(new UsedHeapMemorySpoolRule(spoolUsedHeapMemoryLimit));
318            }
319        }
320
321        LOG.debug("StreamCaching configuration {}", this);
322
323        if (spoolDirectory != null) {
324            LOG.info("StreamCaching in use with spool directory: {} and rules: {}", spoolDirectory.getPath(), spoolRules);
325        } else {
326            LOG.info("StreamCaching in use with rules: {}", spoolRules);
327        }
328    }
329
330    @Override
331    protected void doStop() throws Exception {
332        if (spoolThreshold > 0 & spoolDirectory != null  && isRemoveSpoolDirectoryWhenStopping()) {
333            LOG.debug("Removing spool directory: {}", spoolDirectory);
334            FileUtil.removeDir(spoolDirectory);
335        }
336
337        if (LOG.isDebugEnabled() && statistics.isStatisticsEnabled()) {
338            LOG.debug("Stopping StreamCachingStrategy with statistics: {}", statistics);
339        }
340
341        statistics.reset();
342    }
343
344    @Override
345    public String toString() {
346        return "DefaultStreamCachingStrategy["
347            + "spoolDirectory=" + spoolDirectory
348            + ", spoolCihper=" + spoolCihper
349            + ", spoolThreshold=" + spoolThreshold
350            + ", spoolUsedHeapMemoryThreshold=" + spoolUsedHeapMemoryThreshold
351            + ", bufferSize=" + bufferSize
352            + ", anySpoolRules=" + anySpoolRules + "]";
353    }
354
355    private final class FixedThresholdSpoolRule implements SpoolRule {
356
357        public boolean shouldSpoolCache(long length) {
358            if (spoolThreshold > 0 && length > spoolThreshold) {
359                LOG.trace("Should spool cache fixed threshold {} > {} -> true", length, spoolThreshold);
360                return true;
361            }
362            return false;
363        }
364
365        public String toString() {
366            if (spoolThreshold < 1024) {
367                return "Spool > " + spoolThreshold + " bytes body size";
368            } else {
369                return "Spool > " + (spoolThreshold >> 10) + "K body size";
370            }
371        }
372    }
373
374    private final class UsedHeapMemorySpoolRule implements SpoolRule {
375
376        private final MemoryMXBean heapUsage;
377        private final SpoolUsedHeapMemoryLimit limit;
378
379        private UsedHeapMemorySpoolRule(SpoolUsedHeapMemoryLimit limit) {
380            this.limit = limit;
381            this.heapUsage = ManagementFactory.getMemoryMXBean();
382        }
383
384        public boolean shouldSpoolCache(long length) {
385            if (spoolUsedHeapMemoryThreshold > 0) {
386                // must use double to calculate with decimals for the percentage
387                double used = heapUsage.getHeapMemoryUsage().getUsed();
388                double upper = limit == SpoolUsedHeapMemoryLimit.Committed
389                    ? heapUsage.getHeapMemoryUsage().getCommitted() : heapUsage.getHeapMemoryUsage().getMax();
390                double calc = (used / upper) * 100;
391                int percentage = (int) calc;
392
393                if (LOG.isTraceEnabled()) {
394                    long u = heapUsage.getHeapMemoryUsage().getUsed();
395                    long c = heapUsage.getHeapMemoryUsage().getCommitted();
396                    long m = heapUsage.getHeapMemoryUsage().getMax();
397                    LOG.trace("Heap memory: [used={}M ({}%), committed={}M, max={}M]", u >> 20, percentage, c >> 20, m >> 20);
398                }
399
400                if (percentage > spoolUsedHeapMemoryThreshold) {
401                    LOG.trace("Should spool cache heap memory threshold {} > {} -> true", percentage, spoolUsedHeapMemoryThreshold);
402                    return true;
403                }
404            }
405            return false;
406        }
407
408        public String toString() {
409            return "Spool > " + spoolUsedHeapMemoryThreshold + "% used of " + limit + " heap memory";
410        }
411    }
412
413    /**
414     * Represents utilization statistics.
415     */
416    private static final class UtilizationStatistics implements Statistics {
417
418        private boolean statisticsEnabled;
419        private volatile long memoryCounter;
420        private volatile long memorySize;
421        private volatile long memoryAverageSize;
422        private volatile long spoolCounter;
423        private volatile long spoolSize;
424        private volatile long spoolAverageSize;
425
426        synchronized void updateMemory(long size) {
427            memoryCounter++;
428            memorySize += size;
429            memoryAverageSize = memorySize / memoryCounter;
430        }
431
432        synchronized void updateSpool(long size) {
433            spoolCounter++;
434            spoolSize += size;
435            spoolAverageSize = spoolSize / spoolCounter;
436        }
437
438        public long getCacheMemoryCounter() {
439            return memoryCounter;
440        }
441
442        public long getCacheMemorySize() {
443            return memorySize;
444        }
445
446        public long getCacheMemoryAverageSize() {
447            return memoryAverageSize;
448        }
449
450        public long getCacheSpoolCounter() {
451            return spoolCounter;
452        }
453
454        public long getCacheSpoolSize() {
455            return spoolSize;
456        }
457
458        public long getCacheSpoolAverageSize() {
459            return spoolAverageSize;
460        }
461
462        public synchronized void reset() {
463            memoryCounter = 0;
464            memorySize = 0;
465            memoryAverageSize = 0;
466            spoolCounter = 0;
467            spoolSize = 0;
468            spoolAverageSize = 0;
469        }
470
471        public boolean isStatisticsEnabled() {
472            return statisticsEnabled;
473        }
474
475        public void setStatisticsEnabled(boolean statisticsEnabled) {
476            this.statisticsEnabled = statisticsEnabled;
477        }
478
479        public String toString() {
480            return String.format("[memoryCounter=%s, memorySize=%s, memoryAverageSize=%s, spoolCounter=%s, spoolSize=%s, spoolAverageSize=%s]",
481                    memoryCounter, memorySize, memoryAverageSize, spoolCounter, spoolSize, spoolAverageSize);
482        }
483    }
484
485}