001package org.apache.commons.jcs3.auxiliary.disk.block; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.File; 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Set; 031import java.util.concurrent.ScheduledExecutorService; 032import java.util.concurrent.ScheduledFuture; 033import java.util.concurrent.TimeUnit; 034import java.util.concurrent.locks.ReentrantReadWriteLock; 035import java.util.stream.Collectors; 036 037import org.apache.commons.jcs3.auxiliary.AuxiliaryCacheAttributes; 038import org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache; 039import org.apache.commons.jcs3.engine.behavior.ICacheElement; 040import org.apache.commons.jcs3.engine.behavior.IElementSerializer; 041import org.apache.commons.jcs3.engine.behavior.IRequireScheduler; 042import org.apache.commons.jcs3.engine.control.group.GroupAttrName; 043import org.apache.commons.jcs3.engine.control.group.GroupId; 044import org.apache.commons.jcs3.engine.stats.StatElement; 045import org.apache.commons.jcs3.engine.stats.Stats; 046import org.apache.commons.jcs3.engine.stats.behavior.IStatElement; 047import org.apache.commons.jcs3.engine.stats.behavior.IStats; 048import org.apache.commons.jcs3.log.Log; 049import org.apache.commons.jcs3.log.LogManager; 050import org.apache.commons.jcs3.utils.serialization.StandardSerializer; 051 052/** 053 * There is one BlockDiskCache per region. It manages the key and data store. 054 */ 055public class BlockDiskCache<K, V> 056 extends AbstractDiskCache<K, V> 057 implements IRequireScheduler 058{ 059 /** The logger. */ 060 private static final Log log = LogManager.getLog( BlockDiskCache.class ); 061 062 /** The name to prefix all log messages with. */ 063 private final String logCacheName; 064 065 /** The name of the file to store data. */ 066 private final String fileName; 067 068 /** The data access object */ 069 private BlockDisk dataFile; 070 071 /** Attributes governing the behavior of the block disk cache. */ 072 private final BlockDiskCacheAttributes blockDiskCacheAttributes; 073 074 /** The root directory for keys and data. */ 075 private final File rootDirectory; 076 077 /** Store, loads, and persists the keys */ 078 private BlockDiskKeyStore<K> keyStore; 079 080 /** 081 * Use this lock to synchronize reads and writes to the underlying storage mechanism. We don't 082 * need a reentrant lock, since we only lock one level. 083 */ 084 private final ReentrantReadWriteLock storageLock = new ReentrantReadWriteLock(); 085 086 private ScheduledFuture<?> future; 087 088 /** 089 * Constructs the BlockDisk after setting up the root directory. 090 * <p> 091 * @param cacheAttributes 092 */ 093 public BlockDiskCache( final BlockDiskCacheAttributes cacheAttributes ) 094 { 095 this( cacheAttributes, new StandardSerializer() ); 096 } 097 098 /** 099 * Constructs the BlockDisk after setting up the root directory. 100 * <p> 101 * @param cacheAttributes 102 * @param elementSerializer used if supplied, the super's super will not set a null 103 */ 104 public BlockDiskCache( final BlockDiskCacheAttributes cacheAttributes, final IElementSerializer elementSerializer ) 105 { 106 super( cacheAttributes ); 107 setElementSerializer( elementSerializer ); 108 109 this.blockDiskCacheAttributes = cacheAttributes; 110 this.logCacheName = "Region [" + getCacheName() + "] "; 111 112 log.info("{0}: Constructing BlockDiskCache with attributes {1}", logCacheName, cacheAttributes ); 113 114 // Make a clean file name 115 this.fileName = getCacheName().replaceAll("[^a-zA-Z0-9-_\\.]", "_"); 116 this.rootDirectory = cacheAttributes.getDiskPath(); 117 118 log.info("{0}: Cache file root directory: [{1}]", logCacheName, rootDirectory); 119 120 try 121 { 122 if ( this.blockDiskCacheAttributes.getBlockSizeBytes() > 0 ) 123 { 124 this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ), 125 this.blockDiskCacheAttributes.getBlockSizeBytes(), 126 getElementSerializer() ); 127 } 128 else 129 { 130 this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ), 131 getElementSerializer() ); 132 } 133 134 keyStore = new BlockDiskKeyStore<>( this.blockDiskCacheAttributes, this ); 135 136 final boolean alright = verifyDisk(); 137 138 if ( keyStore.isEmpty() || !alright ) 139 { 140 this.reset(); 141 } 142 143 // Initialization finished successfully, so set alive to true. 144 setAlive(true); 145 log.info("{0}: Block Disk Cache is alive.", logCacheName); 146 } 147 catch ( final IOException e ) 148 { 149 log.error("{0}: Failure initializing for fileName: {1} and root directory: {2}", 150 logCacheName, fileName, rootDirectory, e); 151 } 152 } 153 154 /** 155 * @see org.apache.commons.jcs3.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService) 156 */ 157 @Override 158 public void setScheduledExecutorService(final ScheduledExecutorService scheduledExecutor) 159 { 160 // add this region to the persistence thread. 161 // TODO we might need to stagger this a bit. 162 if ( this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds() > 0 ) 163 { 164 future = scheduledExecutor.scheduleAtFixedRate(keyStore::saveKeys, 165 this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(), 166 this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(), 167 TimeUnit.SECONDS); 168 } 169 } 170 171 /** 172 * We need to verify that the file on disk uses the same block size and that the file is the 173 * proper size. 174 * <p> 175 * @return true if it looks ok 176 */ 177 protected boolean verifyDisk() 178 { 179 boolean alright = false; 180 // simply try to read a few. If it works, then the file is probably ok. 181 // TODO add more. 182 183 storageLock.readLock().lock(); 184 185 try 186 { 187 this.keyStore.entrySet().stream() 188 .limit(100) 189 .forEach(entry -> { 190 try 191 { 192 final Object data = this.dataFile.read(entry.getValue()); 193 if ( data == null ) 194 { 195 throw new IOException("Data is null"); 196 } 197 } 198 catch (final IOException | ClassNotFoundException e) 199 { 200 throw new RuntimeException(logCacheName 201 + " Couldn't find data for key [" + entry.getKey() + "]", e); 202 } 203 }); 204 alright = true; 205 } 206 catch ( final Exception e ) 207 { 208 log.warn("{0}: Problem verifying disk.", logCacheName, e); 209 alright = false; 210 } 211 finally 212 { 213 storageLock.readLock().unlock(); 214 } 215 216 return alright; 217 } 218 219 /** 220 * Return the keys in this cache. 221 * <p> 222 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getKeySet() 223 */ 224 @Override 225 public Set<K> getKeySet() throws IOException 226 { 227 final HashSet<K> keys = new HashSet<>(); 228 229 storageLock.readLock().lock(); 230 231 try 232 { 233 keys.addAll(this.keyStore.keySet()); 234 } 235 finally 236 { 237 storageLock.readLock().unlock(); 238 } 239 240 return keys; 241 } 242 243 /** 244 * Gets matching items from the cache. 245 * <p> 246 * @param pattern 247 * @return a map of K key to ICacheElement<K, V> element, or an empty map if there is no 248 * data in cache matching keys 249 */ 250 @Override 251 public Map<K, ICacheElement<K, V>> processGetMatching( final String pattern ) 252 { 253 Set<K> keyArray = null; 254 storageLock.readLock().lock(); 255 try 256 { 257 keyArray = new HashSet<>(keyStore.keySet()); 258 } 259 finally 260 { 261 storageLock.readLock().unlock(); 262 } 263 264 final Set<K> matchingKeys = getKeyMatcher().getMatchingKeysFromArray( pattern, keyArray ); 265 266 return matchingKeys.stream() 267 .collect(Collectors.toMap( 268 key -> key, 269 this::processGet)).entrySet().stream() 270 .filter(entry -> entry.getValue() != null) 271 .collect(Collectors.toMap( 272 Entry::getKey, 273 Entry::getValue)); 274 } 275 276 /** 277 * Returns the number of keys. 278 * <p> 279 * (non-Javadoc) 280 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getSize() 281 */ 282 @Override 283 public int getSize() 284 { 285 return this.keyStore.size(); 286 } 287 288 /** 289 * Gets the ICacheElement<K, V> for the key if it is in the cache. The program flow is as follows: 290 * <ol> 291 * <li>Make sure the disk cache is alive.</li> <li>Get a read lock.</li> <li>See if the key is 292 * in the key store.</li> <li>If we found a key, ask the BlockDisk for the object at the 293 * blocks..</li> <li>Release the lock.</li> 294 * </ol> 295 * @param key 296 * @return ICacheElement 297 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#get(Object) 298 */ 299 @Override 300 protected ICacheElement<K, V> processGet( final K key ) 301 { 302 if ( !isAlive() ) 303 { 304 log.debug("{0}: No longer alive so returning null for key = {1}", logCacheName, key ); 305 return null; 306 } 307 308 log.debug("{0}: Trying to get from disk: {1}", logCacheName, key ); 309 310 ICacheElement<K, V> object = null; 311 312 313 try 314 { 315 storageLock.readLock().lock(); 316 try { 317 final int[] ded = this.keyStore.get( key ); 318 if ( ded != null ) 319 { 320 object = this.dataFile.read( ded ); 321 } 322 } finally { 323 storageLock.readLock().unlock(); 324 } 325 326 } 327 catch ( final IOException ioe ) 328 { 329 log.error("{0}: Failure getting from disk--IOException, key = {1}", logCacheName, key, ioe ); 330 reset(); 331 } 332 catch ( final Exception e ) 333 { 334 log.error("{0}: Failure getting from disk, key = {1}", logCacheName, key, e ); 335 } 336 return object; 337 } 338 339 /** 340 * Writes an element to disk. The program flow is as follows: 341 * <ol> 342 * <li>Acquire write lock.</li> <li>See id an item exists for this key.</li> <li>If an item 343 * already exists, add its blocks to the remove list.</li> <li>Have the Block disk write the 344 * item.</li> <li>Create a descriptor and add it to the key map.</li> <li>Release the write 345 * lock.</li> 346 * </ol> 347 * @param element 348 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#update(ICacheElement) 349 */ 350 @Override 351 protected void processUpdate( final ICacheElement<K, V> element ) 352 { 353 if ( !isAlive() ) 354 { 355 log.debug("{0}: No longer alive; aborting put of key = {1}", 356 () -> logCacheName, element::getKey); 357 return; 358 } 359 360 int[] old = null; 361 362 // make sure this only locks for one particular cache region 363 storageLock.writeLock().lock(); 364 365 try 366 { 367 old = this.keyStore.get( element.getKey() ); 368 369 if ( old != null ) 370 { 371 this.dataFile.freeBlocks( old ); 372 } 373 374 final int[] blocks = this.dataFile.write( element ); 375 376 this.keyStore.put( element.getKey(), blocks ); 377 378 log.debug("{0}: Put to file [{1}] key [{2}]", () -> logCacheName, 379 () -> fileName, element::getKey); 380 } 381 catch ( final IOException e ) 382 { 383 log.error("{0}: Failure updating element, key: {1} old: {2}", 384 logCacheName, element.getKey(), Arrays.toString(old), e); 385 } 386 finally 387 { 388 storageLock.writeLock().unlock(); 389 } 390 391 log.debug("{0}: Storing element on disk, key: {1}", () -> logCacheName, 392 element::getKey); 393 } 394 395 /** 396 * Returns true if the removal was successful; or false if there is nothing to remove. Current 397 * implementation always result in a disk orphan. 398 * <p> 399 * @param key 400 * @return true if removed anything 401 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#remove(Object) 402 */ 403 @Override 404 protected boolean processRemove( final K key ) 405 { 406 if ( !isAlive() ) 407 { 408 log.debug("{0}: No longer alive so returning false for key = {1}", logCacheName, key ); 409 return false; 410 } 411 412 boolean reset = false; 413 boolean removed = false; 414 415 storageLock.writeLock().lock(); 416 417 try 418 { 419 if (key instanceof String && key.toString().endsWith(NAME_COMPONENT_DELIMITER)) 420 { 421 removed = performPartialKeyRemoval((String) key); 422 } 423 else if (key instanceof GroupAttrName && ((GroupAttrName<?>) key).attrName == null) 424 { 425 removed = performGroupRemoval(((GroupAttrName<?>) key).groupId); 426 } 427 else 428 { 429 removed = performSingleKeyRemoval(key); 430 } 431 } 432 catch ( final Exception e ) 433 { 434 log.error("{0}: Problem removing element.", logCacheName, e ); 435 reset = true; 436 } 437 finally 438 { 439 storageLock.writeLock().unlock(); 440 } 441 442 if ( reset ) 443 { 444 reset(); 445 } 446 447 return removed; 448 } 449 450 /** 451 * Remove all elements from the group. This does not use the iterator to remove. It builds a 452 * list of group elements and then removes them one by one. 453 * <p> 454 * This operates under a lock obtained in doRemove(). 455 * <p> 456 * 457 * @param key 458 * @return true if an element was removed 459 */ 460 private boolean performGroupRemoval(final GroupId key) 461 { 462 // remove all keys of the same name group. 463 final List<K> itemsToRemove = keyStore.keySet() 464 .stream() 465 .filter(k -> k instanceof GroupAttrName && ((GroupAttrName<?>) k).groupId.equals(key)) 466 .collect(Collectors.toList()); 467 468 // remove matches. 469 // Don't add to recycle bin here 470 // https://issues.apache.org/jira/browse/JCS-67 471 itemsToRemove.forEach(this::performSingleKeyRemoval); 472 // TODO this needs to update the remove count separately 473 474 return !itemsToRemove.isEmpty(); 475 } 476 477 /** 478 * Iterates over the keyset. Builds a list of matches. Removes all the keys in the list. Does 479 * not remove via the iterator, since the map impl may not support it. 480 * <p> 481 * This operates under a lock obtained in doRemove(). 482 * <p> 483 * 484 * @param key 485 * @return true if there was a match 486 */ 487 private boolean performPartialKeyRemoval(final String key) 488 { 489 // remove all keys of the same name hierarchy. 490 final List<K> itemsToRemove = keyStore.keySet() 491 .stream() 492 .filter(k -> k instanceof String && k.toString().startsWith(key)) 493 .collect(Collectors.toList()); 494 495 // remove matches. 496 // Don't add to recycle bin here 497 // https://issues.apache.org/jira/browse/JCS-67 498 itemsToRemove.forEach(this::performSingleKeyRemoval); 499 // TODO this needs to update the remove count separately 500 501 return !itemsToRemove.isEmpty(); 502 } 503 504 505 private boolean performSingleKeyRemoval(final K key) { 506 final boolean removed; 507 // remove single item. 508 final int[] ded = this.keyStore.remove( key ); 509 removed = ded != null; 510 if ( removed ) 511 { 512 this.dataFile.freeBlocks( ded ); 513 } 514 515 log.debug("{0}: Disk removal: Removed from key hash, key [{1}] removed = {2}", 516 logCacheName, key, removed); 517 return removed; 518 } 519 520 /** 521 * Resets the keyfile, the disk file, and the memory key map. 522 * <p> 523 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#removeAll() 524 */ 525 @Override 526 protected void processRemoveAll() 527 { 528 reset(); 529 } 530 531 /** 532 * Dispose of the disk cache in a background thread. Joins against this thread to put a cap on 533 * the disposal time. 534 * <p> 535 * TODO make dispose window configurable. 536 */ 537 @Override 538 public void processDispose() 539 { 540 final Thread t = new Thread(this::disposeInternal, "BlockDiskCache-DisposalThread" ); 541 t.start(); 542 // wait up to 60 seconds for dispose and then quit if not done. 543 try 544 { 545 t.join( 60 * 1000 ); 546 } 547 catch ( final InterruptedException ex ) 548 { 549 log.error("{0}: Interrupted while waiting for disposal thread to finish.", 550 logCacheName, ex ); 551 } 552 } 553 554 /** 555 * Internal method that handles the disposal. 556 */ 557 protected void disposeInternal() 558 { 559 if ( !isAlive() ) 560 { 561 log.error("{0}: Not alive and dispose was called, filename: {1}", logCacheName, fileName); 562 return; 563 } 564 storageLock.writeLock().lock(); 565 try 566 { 567 // Prevents any interaction with the cache while we're shutting down. 568 setAlive(false); 569 this.keyStore.saveKeys(); 570 571 if (future != null) 572 { 573 future.cancel(true); 574 } 575 576 try 577 { 578 log.debug("{0}: Closing files, base filename: {1}", logCacheName, fileName ); 579 dataFile.close(); 580 // dataFile = null; 581 } 582 catch ( final IOException e ) 583 { 584 log.error("{0}: Failure closing files in dispose, filename: {1}", 585 logCacheName, fileName, e ); 586 } 587 } 588 finally 589 { 590 storageLock.writeLock().unlock(); 591 } 592 593 log.info("{0}: Shutdown complete.", logCacheName); 594 } 595 596 /** 597 * Returns the attributes. 598 * <p> 599 * @see org.apache.commons.jcs3.auxiliary.AuxiliaryCache#getAuxiliaryCacheAttributes() 600 */ 601 @Override 602 public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes() 603 { 604 return this.blockDiskCacheAttributes; 605 } 606 607 /** 608 * Reset effectively clears the disk cache, creating new files, recycle bins, and keymaps. 609 * <p> 610 * It can be used to handle errors by last resort, force content update, or remove all. 611 */ 612 private void reset() 613 { 614 log.info("{0}: Resetting cache", logCacheName); 615 616 try 617 { 618 storageLock.writeLock().lock(); 619 620 this.keyStore.reset(); 621 622 if ( dataFile != null ) 623 { 624 dataFile.reset(); 625 } 626 } 627 catch ( final IOException e ) 628 { 629 log.error("{0}: Failure resetting state", logCacheName, e ); 630 } 631 finally 632 { 633 storageLock.writeLock().unlock(); 634 } 635 } 636 637 /** 638 * Add these blocks to the emptyBlock list. 639 * <p> 640 * @param blocksToFree 641 */ 642 protected void freeBlocks( final int[] blocksToFree ) 643 { 644 this.dataFile.freeBlocks( blocksToFree ); 645 } 646 647 /** 648 * Returns info about the disk cache. 649 * <p> 650 * @see org.apache.commons.jcs3.auxiliary.AuxiliaryCache#getStatistics() 651 */ 652 @Override 653 public IStats getStatistics() 654 { 655 final IStats stats = new Stats(); 656 stats.setTypeName( "Block Disk Cache" ); 657 658 final ArrayList<IStatElement<?>> elems = new ArrayList<>(); 659 660 elems.add(new StatElement<>( "Is Alive", Boolean.valueOf(isAlive()) ) ); 661 elems.add(new StatElement<>( "Key Map Size", Integer.valueOf(this.keyStore.size()) ) ); 662 663 if (this.dataFile != null) 664 { 665 try 666 { 667 elems.add(new StatElement<>( "Data File Length", Long.valueOf(this.dataFile.length()) ) ); 668 } 669 catch ( final IOException e ) 670 { 671 log.error( e ); 672 } 673 674 elems.add(new StatElement<>( "Block Size Bytes", 675 Integer.valueOf(this.dataFile.getBlockSizeBytes()) ) ); 676 elems.add(new StatElement<>( "Number Of Blocks", 677 Integer.valueOf(this.dataFile.getNumberOfBlocks()) ) ); 678 elems.add(new StatElement<>( "Average Put Size Bytes", 679 Long.valueOf(this.dataFile.getAveragePutSizeBytes()) ) ); 680 elems.add(new StatElement<>( "Empty Blocks", 681 Integer.valueOf(this.dataFile.getEmptyBlocks()) ) ); 682 } 683 684 // get the stats from the super too 685 final IStats sStats = super.getStatistics(); 686 elems.addAll(sStats.getStatElements()); 687 688 stats.setStatElements( elems ); 689 690 return stats; 691 } 692 693 /** 694 * This is used by the event logging. 695 * <p> 696 * @return the location of the disk, either path or ip. 697 */ 698 @Override 699 protected String getDiskLocation() 700 { 701 return dataFile.getFilePath(); 702 } 703}