001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.messages.ExtensionMessages.*; 030 031import java.lang.ref.Reference; 032import java.lang.ref.ReferenceQueue; 033import java.lang.ref.SoftReference; 034import java.util.ArrayList; 035import java.util.Collections; 036import java.util.HashSet; 037import java.util.List; 038import java.util.Set; 039import java.util.concurrent.ConcurrentHashMap; 040import java.util.concurrent.ConcurrentMap; 041 042import org.forgerock.i18n.LocalizableMessage; 043import org.forgerock.i18n.slf4j.LocalizedLogger; 044import org.forgerock.opendj.config.server.ConfigException; 045import org.forgerock.util.Utils; 046import org.opends.server.admin.server.ConfigurationChangeListener; 047import org.opends.server.admin.std.server.EntryCacheCfg; 048import org.opends.server.admin.std.server.SoftReferenceEntryCacheCfg; 049import org.opends.server.api.Backend; 050import org.opends.server.api.DirectoryThread; 051import org.opends.server.api.EntryCache; 052import org.opends.server.core.DirectoryServer; 053import org.opends.server.types.Attribute; 054import org.opends.server.types.CacheEntry; 055import org.forgerock.opendj.config.server.ConfigChangeResult; 056import org.opends.server.types.DN; 057import org.opends.server.types.Entry; 058import org.opends.server.types.InitializationException; 059import org.opends.server.types.SearchFilter; 060import org.opends.server.util.ServerConstants; 061 062/** 063 * This class defines a Directory Server entry cache that uses soft references 064 * to manage objects in a way that will allow them to be freed if the JVM is 065 * running low on memory. 066 */ 067public class SoftReferenceEntryCache 068 extends EntryCache <SoftReferenceEntryCacheCfg> 069 implements 070 ConfigurationChangeListener<SoftReferenceEntryCacheCfg>, 071 Runnable 072{ 073 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 074 075 /** The mapping between entry DNs and their corresponding entries. */ 076 private ConcurrentMap<DN, Reference<CacheEntry>> dnMap; 077 078 /** The mapping between backend+ID and their corresponding entries. */ 079 private ConcurrentMap<String, ConcurrentMap<Long, Reference<CacheEntry>>> idMap; 080 081 /** 082 * The reference queue that will be used to notify us whenever a soft 083 * reference is freed. 084 */ 085 private ReferenceQueue<CacheEntry> referenceQueue; 086 087 /** Currently registered configuration object. */ 088 private SoftReferenceEntryCacheCfg registeredConfiguration; 089 090 private Thread cleanerThread; 091 private volatile boolean shutdown; 092 093 094 095 /** 096 * Creates a new instance of this soft reference entry cache. All 097 * initialization should be performed in the <CODE>initializeEntryCache</CODE> 098 * method. 099 */ 100 public SoftReferenceEntryCache() 101 { 102 super(); 103 104 dnMap = new ConcurrentHashMap<>(); 105 idMap = new ConcurrentHashMap<>(); 106 107 setExcludeFilters(new HashSet<SearchFilter>()); 108 setIncludeFilters(new HashSet<SearchFilter>()); 109 referenceQueue = new ReferenceQueue<>(); 110 } 111 112 /** {@inheritDoc} */ 113 @Override 114 public void initializeEntryCache( 115 SoftReferenceEntryCacheCfg configuration 116 ) 117 throws ConfigException, InitializationException 118 { 119 cleanerThread = new DirectoryThread(this, 120 "Soft Reference Entry Cache Cleaner"); 121 cleanerThread.setDaemon(true); 122 cleanerThread.start(); 123 124 registeredConfiguration = configuration; 125 configuration.addSoftReferenceChangeListener (this); 126 127 dnMap.clear(); 128 idMap.clear(); 129 130 // Read configuration and apply changes. 131 boolean applyChanges = true; 132 List<LocalizableMessage> errorMessages = new ArrayList<>(); 133 EntryCacheCommon.ConfigErrorHandler errorHandler = 134 EntryCacheCommon.getConfigErrorHandler ( 135 EntryCacheCommon.ConfigPhase.PHASE_INIT, null, errorMessages 136 ); 137 if (!processEntryCacheConfig(configuration, applyChanges, errorHandler)) { 138 String buffer = Utils.joinAsString(". ", errorMessages); 139 throw new ConfigException(ERR_SOFTREFCACHE_CANNOT_INITIALIZE.get(buffer)); 140 } 141 } 142 143 /** {@inheritDoc} */ 144 @Override 145 public synchronized void finalizeEntryCache() 146 { 147 registeredConfiguration.removeSoftReferenceChangeListener (this); 148 149 shutdown = true; 150 151 dnMap.clear(); 152 idMap.clear(); 153 if (cleanerThread != null) { 154 for (int i = 0; cleanerThread.isAlive() && i < 5; i++) { 155 cleanerThread.interrupt(); 156 try { 157 cleanerThread.join(10); 158 } catch (InterruptedException e) { 159 // We'll exit eventually. 160 } 161 } 162 cleanerThread = null; 163 } 164 } 165 166 /** {@inheritDoc} */ 167 @Override 168 public boolean containsEntry(DN entryDN) 169 { 170 return entryDN != null && dnMap.containsKey(entryDN); 171 } 172 173 /** {@inheritDoc} */ 174 @Override 175 public Entry getEntry(DN entryDN) 176 { 177 Reference<CacheEntry> ref = dnMap.get(entryDN); 178 if (ref == null) 179 { 180 // Indicate cache miss. 181 cacheMisses.getAndIncrement(); 182 return null; 183 } 184 CacheEntry cacheEntry = ref.get(); 185 if (cacheEntry == null) 186 { 187 // Indicate cache miss. 188 cacheMisses.getAndIncrement(); 189 return null; 190 } 191 // Indicate cache hit. 192 cacheHits.getAndIncrement(); 193 return cacheEntry.getEntry(); 194 } 195 196 /** {@inheritDoc} */ 197 @Override 198 public long getEntryID(DN entryDN) 199 { 200 Reference<CacheEntry> ref = dnMap.get(entryDN); 201 if (ref != null) 202 { 203 CacheEntry cacheEntry = ref.get(); 204 return cacheEntry != null ? cacheEntry.getEntryID() : -1; 205 } 206 return -1; 207 } 208 209 /** {@inheritDoc} */ 210 @Override 211 public DN getEntryDN(String backendID, long entryID) 212 { 213 // Locate specific backend map and return the entry DN by ID. 214 ConcurrentMap<Long, Reference<CacheEntry>> backendMap = idMap.get(backendID); 215 if (backendMap != null) { 216 Reference<CacheEntry> ref = backendMap.get(entryID); 217 if (ref != null) { 218 CacheEntry cacheEntry = ref.get(); 219 if (cacheEntry != null) { 220 return cacheEntry.getDN(); 221 } 222 } 223 } 224 return null; 225 } 226 227 /** {@inheritDoc} */ 228 @Override 229 public void putEntry(Entry entry, String backendID, long entryID) 230 { 231 // Create the cache entry based on the provided information. 232 CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID); 233 Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue); 234 235 Reference<CacheEntry> oldRef = dnMap.put(entry.getName(), ref); 236 if (oldRef != null) 237 { 238 oldRef.clear(); 239 } 240 241 ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID); 242 if (map == null) 243 { 244 map = new ConcurrentHashMap<>(); 245 map.put(entryID, ref); 246 idMap.put(backendID, map); 247 } 248 else 249 { 250 oldRef = map.put(entryID, ref); 251 if (oldRef != null) 252 { 253 oldRef.clear(); 254 } 255 } 256 } 257 258 /** {@inheritDoc} */ 259 @Override 260 public boolean putEntryIfAbsent(Entry entry, String backendID, long entryID) 261 { 262 // See if the entry already exists. If so, then return false. 263 if (dnMap.containsKey(entry.getName())) 264 { 265 return false; 266 } 267 268 269 // Create the cache entry based on the provided information. 270 CacheEntry cacheEntry = new CacheEntry(entry, backendID, entryID); 271 Reference<CacheEntry> ref = new SoftReference<>(cacheEntry, referenceQueue); 272 273 dnMap.put(entry.getName(), ref); 274 275 ConcurrentMap<Long,Reference<CacheEntry>> map = idMap.get(backendID); 276 if (map == null) 277 { 278 map = new ConcurrentHashMap<>(); 279 map.put(entryID, ref); 280 idMap.put(backendID, map); 281 } 282 else 283 { 284 map.put(entryID, ref); 285 } 286 287 return true; 288 } 289 290 /** {@inheritDoc} */ 291 @Override 292 public void removeEntry(DN entryDN) 293 { 294 Reference<CacheEntry> ref = dnMap.remove(entryDN); 295 if (ref != null) 296 { 297 ref.clear(); 298 299 CacheEntry cacheEntry = ref.get(); 300 if (cacheEntry != null) 301 { 302 final String backendID = cacheEntry.getBackendID(); 303 304 ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID); 305 if (map != null) 306 { 307 ref = map.remove(cacheEntry.getEntryID()); 308 if (ref != null) 309 { 310 ref.clear(); 311 } 312 // If this backend becomes empty now remove 313 // it from the idMap map. 314 if (map.isEmpty()) 315 { 316 idMap.remove(backendID); 317 } 318 } 319 } 320 } 321 } 322 323 /** {@inheritDoc} */ 324 @Override 325 public void clear() 326 { 327 dnMap.clear(); 328 idMap.clear(); 329 } 330 331 /** {@inheritDoc} */ 332 @Override 333 public void clearBackend(String backendID) 334 { 335 // FIXME -- Would it be better just to dump everything? 336 final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.remove(backendID); 337 if (map != null) 338 { 339 for (Reference<CacheEntry> ref : map.values()) 340 { 341 final CacheEntry cacheEntry = ref.get(); 342 if (cacheEntry != null) 343 { 344 dnMap.remove(cacheEntry.getDN()); 345 } 346 347 ref.clear(); 348 } 349 350 map.clear(); 351 } 352 } 353 354 /** {@inheritDoc} */ 355 @Override 356 public void clearSubtree(DN baseDN) 357 { 358 // Determine the backend used to hold the specified base DN and clear it. 359 Backend<?> backend = DirectoryServer.getBackend(baseDN); 360 if (backend == null) 361 { 362 // FIXME -- Should we clear everything just to be safe? 363 } 364 else 365 { 366 clearBackend(backend.getBackendID()); 367 } 368 } 369 370 /** {@inheritDoc} */ 371 @Override 372 public void handleLowMemory() 373 { 374 // This function should automatically be taken care of by the nature of the 375 // soft references used in this cache. 376 // FIXME -- Do we need to do anything at all here? 377 } 378 379 /** {@inheritDoc} */ 380 @Override 381 public boolean isConfigurationAcceptable(EntryCacheCfg configuration, 382 List<LocalizableMessage> unacceptableReasons) 383 { 384 SoftReferenceEntryCacheCfg config = 385 (SoftReferenceEntryCacheCfg) configuration; 386 return isConfigurationChangeAcceptable(config, unacceptableReasons); 387 } 388 389 /** {@inheritDoc} */ 390 @Override 391 public boolean isConfigurationChangeAcceptable( 392 SoftReferenceEntryCacheCfg configuration, 393 List<LocalizableMessage> unacceptableReasons) 394 { 395 boolean applyChanges = false; 396 EntryCacheCommon.ConfigErrorHandler errorHandler = 397 EntryCacheCommon.getConfigErrorHandler ( 398 EntryCacheCommon.ConfigPhase.PHASE_ACCEPTABLE, 399 unacceptableReasons, 400 null 401 ); 402 processEntryCacheConfig (configuration, applyChanges, errorHandler); 403 404 return errorHandler.getIsAcceptable(); 405 } 406 407 /** {@inheritDoc} */ 408 @Override 409 public ConfigChangeResult applyConfigurationChange(SoftReferenceEntryCacheCfg configuration) 410 { 411 boolean applyChanges = true; 412 List<LocalizableMessage> errorMessages = new ArrayList<>(); 413 EntryCacheCommon.ConfigErrorHandler errorHandler = 414 EntryCacheCommon.getConfigErrorHandler ( 415 EntryCacheCommon.ConfigPhase.PHASE_APPLY, null, errorMessages 416 ); 417 // Do not apply changes unless this cache is enabled. 418 if (configuration.isEnabled()) { 419 processEntryCacheConfig (configuration, applyChanges, errorHandler); 420 } 421 422 final ConfigChangeResult changeResult = new ConfigChangeResult(); 423 changeResult.setResultCode(errorHandler.getResultCode()); 424 changeResult.setAdminActionRequired(errorHandler.getIsAdminActionRequired()); 425 changeResult.getMessages().addAll(errorHandler.getErrorMessages()); 426 return changeResult; 427 } 428 429 430 431 /** 432 * Parses the provided configuration and configure the entry cache. 433 * 434 * @param configuration The new configuration containing the changes. 435 * @param applyChanges If true then take into account the new configuration. 436 * @param errorHandler An handler used to report errors. 437 * 438 * @return <CODE>true</CODE> if configuration is acceptable, 439 * or <CODE>false</CODE> otherwise. 440 */ 441 public boolean processEntryCacheConfig( 442 SoftReferenceEntryCacheCfg configuration, 443 boolean applyChanges, 444 EntryCacheCommon.ConfigErrorHandler errorHandler 445 ) 446 { 447 // Local variables to read configuration. 448 DN newConfigEntryDN; 449 Set<SearchFilter> newIncludeFilters = null; 450 Set<SearchFilter> newExcludeFilters = null; 451 452 // Read configuration. 453 newConfigEntryDN = configuration.dn(); 454 455 // Get include and exclude filters. 456 switch (errorHandler.getConfigPhase()) 457 { 458 case PHASE_INIT: 459 case PHASE_ACCEPTABLE: 460 case PHASE_APPLY: 461 newIncludeFilters = EntryCacheCommon.getFilters ( 462 configuration.getIncludeFilter(), 463 ERR_CACHE_INVALID_INCLUDE_FILTER, 464 errorHandler, 465 newConfigEntryDN 466 ); 467 newExcludeFilters = EntryCacheCommon.getFilters ( 468 configuration.getExcludeFilter(), 469 ERR_CACHE_INVALID_EXCLUDE_FILTER, 470 errorHandler, 471 newConfigEntryDN 472 ); 473 break; 474 } 475 476 if (applyChanges && errorHandler.getIsAcceptable()) 477 { 478 setIncludeFilters(newIncludeFilters); 479 setExcludeFilters(newExcludeFilters); 480 481 registeredConfiguration = configuration; 482 } 483 484 return errorHandler.getIsAcceptable(); 485 } 486 487 /** 488 * Operate in a loop, receiving notification of soft references that have been 489 * freed and removing the corresponding entries from the cache. 490 */ 491 @Override 492 public void run() 493 { 494 while (!shutdown) 495 { 496 try 497 { 498 CacheEntry freedEntry = referenceQueue.remove().get(); 499 500 if (freedEntry != null) 501 { 502 Reference<CacheEntry> ref = dnMap.remove(freedEntry.getDN()); 503 504 if (ref != null) 505 { 506 // Note that the entry is there, but it could be a newer version of 507 // the entry so we want to make sure it's the same one. 508 CacheEntry removedEntry = ref.get(); 509 if (removedEntry != freedEntry) 510 { 511 dnMap.putIfAbsent(freedEntry.getDN(), ref); 512 } 513 else 514 { 515 ref.clear(); 516 517 final String backendID = freedEntry.getBackendID(); 518 final ConcurrentMap<Long, Reference<CacheEntry>> map = idMap.get(backendID); 519 if (map != null) 520 { 521 ref = map.remove(freedEntry.getEntryID()); 522 if (ref != null) 523 { 524 ref.clear(); 525 } 526 // If this backend becomes empty now remove 527 // it from the idMap map. 528 if (map.isEmpty()) { 529 idMap.remove(backendID); 530 } 531 } 532 } 533 } 534 } 535 } 536 catch (Exception e) 537 { 538 logger.traceException(e); 539 } 540 } 541 } 542 543 /** {@inheritDoc} */ 544 @Override 545 public List<Attribute> getMonitorData() 546 { 547 try { 548 return EntryCacheCommon.getGenericMonitorData( 549 Long.valueOf(cacheHits.longValue()), 550 // If cache misses is maintained by default cache 551 // get it from there and if not point to itself. 552 DirectoryServer.getEntryCache().getCacheMisses(), 553 null, 554 null, 555 Long.valueOf(dnMap.size()), 556 null 557 ); 558 } catch (Exception e) { 559 logger.traceException(e); 560 return Collections.emptyList(); 561 } 562 } 563 564 /** {@inheritDoc} */ 565 @Override 566 public Long getCacheCount() 567 { 568 return Long.valueOf(dnMap.size()); 569 } 570 571 /** {@inheritDoc} */ 572 @Override 573 public String toVerboseString() 574 { 575 StringBuilder sb = new StringBuilder(); 576 577 // There're no locks in this cache to keep dnMap and idMap in sync. 578 // Examine dnMap only since its more likely to be up to date than idMap. 579 // Do not bother with copies either since this 580 // is SoftReference based implementation. 581 for(Reference<CacheEntry> ce : dnMap.values()) { 582 sb.append(ce.get().getDN()); 583 sb.append(":"); 584 sb.append(ce.get().getEntryID()); 585 sb.append(":"); 586 sb.append(ce.get().getBackendID()); 587 sb.append(ServerConstants.EOL); 588 } 589 590 String verboseString = sb.toString(); 591 return verboseString.length() > 0 ? verboseString : null; 592 } 593}