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}