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 spoolChiper; 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<SpoolRule>(); 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 spoolChiper; 122 } 123 124 public void setSpoolChiper(String spoolChiper) { 125 this.spoolChiper = spoolChiper; 126 } 127 128 public int getBufferSize() { 129 return bufferSize; 130 } 131 132 public void setBufferSize(int bufferSize) { 133 this.bufferSize = bufferSize; 134 } 135 136 public boolean isRemoveSpoolDirectoryWhenStopping() { 137 return removeSpoolDirectoryWhenStopping; 138 } 139 140 public void setRemoveSpoolDirectoryWhenStopping(boolean removeSpoolDirectoryWhenStopping) { 141 this.removeSpoolDirectoryWhenStopping = removeSpoolDirectoryWhenStopping; 142 } 143 144 public boolean isAnySpoolRules() { 145 return anySpoolRules; 146 } 147 148 public void setAnySpoolRules(boolean anySpoolTasks) { 149 this.anySpoolRules = anySpoolTasks; 150 } 151 152 public Statistics getStatistics() { 153 return statistics; 154 } 155 156 public boolean shouldSpoolCache(long length) { 157 if (!enabled || spoolRules.isEmpty()) { 158 return false; 159 } 160 161 boolean all = true; 162 boolean any = false; 163 for (SpoolRule rule : spoolRules) { 164 boolean result = rule.shouldSpoolCache(length); 165 if (!result) { 166 all = false; 167 if (!anySpoolRules) { 168 // no need to check anymore 169 break; 170 } 171 } else { 172 any = true; 173 if (anySpoolRules) { 174 // no need to check anymore 175 break; 176 } 177 } 178 } 179 180 boolean answer = anySpoolRules ? any : all; 181 LOG.debug("Should spool cache {} -> {}", length, answer); 182 return answer; 183 } 184 185 public void addSpoolRule(SpoolRule rule) { 186 spoolRules.add(rule); 187 } 188 189 public StreamCache cache(Exchange exchange) { 190 Message message = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); 191 StreamCache cache = message.getBody(StreamCache.class); 192 if (cache != null) { 193 if (LOG.isTraceEnabled()) { 194 LOG.trace("Cached stream to {} -> {}", cache.inMemory() ? "memory" : "spool", cache); 195 } 196 if (statistics.isStatisticsEnabled()) { 197 try { 198 if (cache.inMemory()) { 199 statistics.updateMemory(cache.length()); 200 } else { 201 statistics.updateSpool(cache.length()); 202 } 203 } catch (Exception e) { 204 LOG.debug("Error updating cache statistics. This exception is ignored.", e); 205 } 206 } 207 } 208 return cache; 209 } 210 211 protected String resolveSpoolDirectory(String path) { 212 String name = camelContext.getManagementNameStrategy().resolveManagementName(path, camelContext.getName(), false); 213 if (name != null) { 214 name = customResolveManagementName(name); 215 } 216 // and then check again with invalid check to ensure all ## is resolved 217 if (name != null) { 218 name = camelContext.getManagementNameStrategy().resolveManagementName(name, camelContext.getName(), true); 219 } 220 return name; 221 } 222 223 protected String customResolveManagementName(String pattern) { 224 if (pattern.contains("#uuid#")) { 225 String uuid = UUID.randomUUID().toString(); 226 pattern = pattern.replaceFirst("#uuid#", uuid); 227 } 228 return FilePathResolver.resolvePath(pattern); 229 } 230 231 @Override 232 protected void doStart() throws Exception { 233 if (!enabled) { 234 LOG.debug("StreamCaching is not enabled"); 235 return; 236 } 237 238 String bufferSize = camelContext.getGlobalOption(BUFFER_SIZE); 239 String hold = camelContext.getGlobalOption(THRESHOLD); 240 String chiper = camelContext.getGlobalOption(CIPHER_TRANSFORMATION); 241 String dir = camelContext.getGlobalOption(TEMP_DIR); 242 243 boolean warn = false; 244 if (bufferSize != null) { 245 warn = true; 246 this.bufferSize = camelContext.getTypeConverter().convertTo(Integer.class, bufferSize); 247 } 248 if (hold != null) { 249 warn = true; 250 this.spoolThreshold = camelContext.getTypeConverter().convertTo(Long.class, hold); 251 } 252 if (chiper != null) { 253 warn = true; 254 this.spoolChiper = chiper; 255 } 256 if (dir != null) { 257 warn = true; 258 this.spoolDirectory = camelContext.getTypeConverter().convertTo(File.class, dir); 259 } 260 if (warn) { 261 LOG.warn("Configuring of StreamCaching using CamelContext properties is deprecated - use StreamCachingStrategy instead."); 262 } 263 264 if (spoolUsedHeapMemoryThreshold > 99) { 265 throw new IllegalArgumentException("SpoolHeapMemoryWatermarkThreshold must not be higher than 99, was: " + spoolUsedHeapMemoryThreshold); 266 } 267 268 // if we can overflow to disk then make sure directory exists / is created 269 if (spoolThreshold > 0 || spoolUsedHeapMemoryThreshold > 0) { 270 271 if (spoolDirectory == null && spoolDirectoryName == null) { 272 throw new IllegalArgumentException("SpoolDirectory must be configured when using SpoolThreshold > 0"); 273 } 274 275 if (spoolDirectory == null) { 276 String name = resolveSpoolDirectory(spoolDirectoryName); 277 if (name != null) { 278 spoolDirectory = new File(name); 279 spoolDirectoryName = null; 280 } else { 281 throw new IllegalStateException("Cannot resolve spool directory from pattern: " + spoolDirectoryName); 282 } 283 } 284 285 if (spoolDirectory.exists()) { 286 if (spoolDirectory.isDirectory()) { 287 LOG.debug("Using spool directory: {}", spoolDirectory); 288 } else { 289 LOG.warn("Spool directory: {} is not a directory. This may cause problems spooling to disk for the stream caching!", spoolDirectory); 290 } 291 } else { 292 boolean created = spoolDirectory.mkdirs(); 293 if (!created) { 294 LOG.warn("Cannot create spool directory: {}. This may cause problems spooling to disk for the stream caching!", spoolDirectory); 295 } else { 296 LOG.debug("Created spool directory: {}", spoolDirectory); 297 } 298 299 } 300 301 if (spoolThreshold > 0) { 302 spoolRules.add(new FixedThresholdSpoolRule()); 303 } 304 if (spoolUsedHeapMemoryThreshold > 0) { 305 if (spoolUsedHeapMemoryLimit == null) { 306 // use max by default 307 spoolUsedHeapMemoryLimit = SpoolUsedHeapMemoryLimit.Max; 308 } 309 spoolRules.add(new UsedHeapMemorySpoolRule(spoolUsedHeapMemoryLimit)); 310 } 311 } 312 313 LOG.debug("StreamCaching configuration {}", this.toString()); 314 315 if (spoolDirectory != null) { 316 LOG.info("StreamCaching in use with spool directory: {} and rules: {}", spoolDirectory.getPath(), spoolRules.toString()); 317 } else { 318 LOG.info("StreamCaching in use with rules: {}", spoolRules.toString()); 319 } 320 } 321 322 @Override 323 protected void doStop() throws Exception { 324 if (spoolThreshold > 0 & spoolDirectory != null && isRemoveSpoolDirectoryWhenStopping()) { 325 LOG.debug("Removing spool directory: {}", spoolDirectory); 326 FileUtil.removeDir(spoolDirectory); 327 } 328 329 if (LOG.isDebugEnabled() && statistics.isStatisticsEnabled()) { 330 LOG.debug("Stopping StreamCachingStrategy with statistics: {}", statistics.toString()); 331 } 332 333 statistics.reset(); 334 } 335 336 @Override 337 public String toString() { 338 return "DefaultStreamCachingStrategy[" 339 + "spoolDirectory=" + spoolDirectory 340 + ", spoolChiper=" + spoolChiper 341 + ", spoolThreshold=" + spoolThreshold 342 + ", spoolUsedHeapMemoryThreshold=" + spoolUsedHeapMemoryThreshold 343 + ", bufferSize=" + bufferSize 344 + ", anySpoolRules=" + anySpoolRules + "]"; 345 } 346 347 private final class FixedThresholdSpoolRule implements SpoolRule { 348 349 public boolean shouldSpoolCache(long length) { 350 if (spoolThreshold > 0 && length > spoolThreshold) { 351 LOG.trace("Should spool cache fixed threshold {} > {} -> true", length, spoolThreshold); 352 return true; 353 } 354 return false; 355 } 356 357 public String toString() { 358 if (spoolThreshold < 1024) { 359 return "Spool > " + spoolThreshold + " bytes body size"; 360 } else { 361 return "Spool > " + (spoolThreshold >> 10) + "K body size"; 362 } 363 } 364 } 365 366 private final class UsedHeapMemorySpoolRule implements SpoolRule { 367 368 private final MemoryMXBean heapUsage; 369 private final SpoolUsedHeapMemoryLimit limit; 370 371 private UsedHeapMemorySpoolRule(SpoolUsedHeapMemoryLimit limit) { 372 this.limit = limit; 373 this.heapUsage = ManagementFactory.getMemoryMXBean(); 374 } 375 376 public boolean shouldSpoolCache(long length) { 377 if (spoolUsedHeapMemoryThreshold > 0) { 378 // must use double to calculate with decimals for the percentage 379 double used = heapUsage.getHeapMemoryUsage().getUsed(); 380 double upper = limit == SpoolUsedHeapMemoryLimit.Committed 381 ? heapUsage.getHeapMemoryUsage().getCommitted() : heapUsage.getHeapMemoryUsage().getMax(); 382 double calc = (used / upper) * 100; 383 int percentage = (int) calc; 384 385 if (LOG.isTraceEnabled()) { 386 long u = heapUsage.getHeapMemoryUsage().getUsed(); 387 long c = heapUsage.getHeapMemoryUsage().getCommitted(); 388 long m = heapUsage.getHeapMemoryUsage().getMax(); 389 LOG.trace("Heap memory: [used={}M ({}%), committed={}M, max={}M]", new Object[]{u >> 20, percentage, c >> 20, m >> 20}); 390 } 391 392 if (percentage > spoolUsedHeapMemoryThreshold) { 393 LOG.trace("Should spool cache heap memory threshold {} > {} -> true", percentage, spoolUsedHeapMemoryThreshold); 394 return true; 395 } 396 } 397 return false; 398 } 399 400 public String toString() { 401 return "Spool > " + spoolUsedHeapMemoryThreshold + "% used of " + limit + " heap memory"; 402 } 403 } 404 405 /** 406 * Represents utilization statistics. 407 */ 408 private static final class UtilizationStatistics implements Statistics { 409 410 private boolean statisticsEnabled; 411 private volatile long memoryCounter; 412 private volatile long memorySize; 413 private volatile long memoryAverageSize; 414 private volatile long spoolCounter; 415 private volatile long spoolSize; 416 private volatile long spoolAverageSize; 417 418 synchronized void updateMemory(long size) { 419 memoryCounter++; 420 memorySize += size; 421 memoryAverageSize = memorySize / memoryCounter; 422 } 423 424 synchronized void updateSpool(long size) { 425 spoolCounter++; 426 spoolSize += size; 427 spoolAverageSize = spoolSize / spoolCounter; 428 } 429 430 public long getCacheMemoryCounter() { 431 return memoryCounter; 432 } 433 434 public long getCacheMemorySize() { 435 return memorySize; 436 } 437 438 public long getCacheMemoryAverageSize() { 439 return memoryAverageSize; 440 } 441 442 public long getCacheSpoolCounter() { 443 return spoolCounter; 444 } 445 446 public long getCacheSpoolSize() { 447 return spoolSize; 448 } 449 450 public long getCacheSpoolAverageSize() { 451 return spoolAverageSize; 452 } 453 454 public synchronized void reset() { 455 memoryCounter = 0; 456 memorySize = 0; 457 memoryAverageSize = 0; 458 spoolCounter = 0; 459 spoolSize = 0; 460 spoolAverageSize = 0; 461 } 462 463 public boolean isStatisticsEnabled() { 464 return statisticsEnabled; 465 } 466 467 public void setStatisticsEnabled(boolean statisticsEnabled) { 468 this.statisticsEnabled = statisticsEnabled; 469 } 470 471 public String toString() { 472 return String.format("[memoryCounter=%s, memorySize=%s, memoryAverageSize=%s, spoolCounter=%s, spoolSize=%s, spoolAverageSize=%s]", 473 memoryCounter, memorySize, memoryAverageSize, spoolCounter, spoolSize, spoolAverageSize); 474 } 475 } 476 477}