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