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