All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.turbo.Turbo Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.turbo;

import org.openide.util.Lookup;
import org.openide.ErrorManager;

import java.util.*;
import java.lang.ref.WeakReference;
import org.netbeans.modules.versioning.util.Utils;

/**
 * Turbo is general purpose entries/attributes dictionary with pluggable
 * layer enabling high scalability disk swapping implementations.
 * It allows to store several name identified values
 * for an entity identified by a key.
 *
 * 

All methods take a key parameter. It * identifies entity to which attribute values are associated. * It must have properly implemented equals(), * hashcode() and toString() (returning * unique string) methods. Key lifetime must be well * understood to set cache strategy efficiently: *

    *
  • MAXIMUM: The implementation monitors key instance lifetime and does * not release data from memory until max. size limit reached or * key instance garbage collected whichever comes sooner. For * unbound caches the key must not be hard referenced from stored * value. * *
  • MINIMUM: For value lookups key's value based equalence is used. * Key's instance lifetime is monitored by instance based equalence. * Reasonable min limit must be set because there can be several * value equalent key instances but only one used key instance * actually stored in cache and lifetime monitored. *
* *

Entry name fully identifies contract between * consumers and providers. Contracts are described elsewhere. * *

The dictionary does not support storing null * values. Writing null value means that given * entry should be invalidated and removed (it actually depends * on externally negotiated contract identified by name). Getting * null as request result means that given value * is not (yet) known or does not exist at all. * * @author Petr Kuzel */ public final class Turbo { /** Default providers registry. */ private static Lookup.Result providers; /** Custom providers 'registry'. */ private final CustomProviders customProviders; private static WeakReference defaultInstance; private List listeners = new ArrayList<>(100); /** memory layer */ private final Memory memory; private final Statistics statistics; private static Environment env; /** * Returns default instance. It's size is driven by * keys lifetime. It shrinks on keys become unreferenced * and grows without bounds on inserting keys. */ public static synchronized Turbo getDefault() { Turbo turbo = null; if (defaultInstance != null) { turbo = (Turbo) defaultInstance.get(); } if (turbo == null) { turbo = new Turbo(null, 47, -1); defaultInstance = new WeakReference(turbo); } return turbo; } /** * Creates new instance with customized providers layer. * @param providers never null * @param min minimum number of entries held by the cache * @param max maximum size or -1 for unbound size * (defined just by key instance lifetime) */ public static synchronized Turbo createCustom(CustomProviders providers, int min, int max) { return new Turbo(providers, min, max); } private Turbo(CustomProviders customProviders, int min, int max) { statistics = Statistics.createInstance(); memory = new Memory(statistics, min, max); this.customProviders = customProviders; if (customProviders == null && providers == null) { Lookup.Template t = new Lookup.Template(TurboProvider.class); synchronized(Turbo.class) { if (env == null) env = new Environment(); } providers = env.getLookup().lookup(t); } } /** Tests can set different environment. Must be called before {@link #getDefault}. */ static synchronized void initEnvironment(Environment environment) { assert env == null; env = environment; providers = null; } /** Logs cache statistics data. */ protected void finalize() throws Throwable { super.finalize(); statistics.shutdown(); } /** * Reads given attribute for given entity key. * @param key a entity key, never null * @param name identifies requested entry, never null * @return entry value or null if it does not exist or unknown. */ public Object readEntry(Object key, String name) { statistics.attributeRequest(); // check memory cache if (memory.existsEntry(key, name)) { Object value = memory.get(key, name); statistics.memoryHit(); return value; } // iterate over providers List speculative = new ArrayList(57); Object value = loadEntry(key, name, speculative); memory.put(key, name, value != null ? value : Memory.NULL); // XXX should fire here? yes if name availability changes should be // dispatched to clients that have not called prepare otherwise NO. // refire speculative results, can be optimized later on to fire // them lazily on prepareAttribute or isPrepared calls Iterator it = speculative.iterator(); while (it.hasNext()) { Object[] next = (Object[]) it.next(); Object sKey = next[0]; String sName = (String) next[1]; Object sValue = next[2]; assert sKey != null; assert sName != null; fireEntryChange(sKey, sName, sValue); } return value; } private Iterator providers() { if (customProviders == null) { Collection plugins = providers.allInstances(); List all = new ArrayList(plugins.size() +1); all.addAll(plugins); all.add(DefaultTurboProvider.getDefault()); return all.iterator(); } else { return customProviders.providers(); } } /** * Iterate over providers asking for attribute values */ private Object loadEntry(Object key, String name, List speculative) { TurboProvider provider; Iterator it = providers(); while (it.hasNext()) { provider = (TurboProvider) it.next(); try { if (provider.recognizesAttribute(name) && provider.recognizesEntity(key)) { TurboProvider.MemoryCache cache = TurboProvider.MemoryCache.createDefault(memory, speculative); Object value = provider.readEntry(key, name, cache); statistics.providerHit(); return value; } } catch (ThreadDeath td) { throw td; } catch (Throwable t) { // error in provider ErrorManager.getDefault().annotate(t, "Error in provider " + provider + ", skipping... "); // NOI18N ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t); // XXX in Junit mode writes to stdout omitting annotation!!! } } return null; } /** * Writes given attribute, value pair and notifies all listeners. * Written value is stored both into memory and providers layer. * The call speed depends on actually used provider. However do not * rely on synchronous provider call. In future it may return and * fire immediately on writing into memory layer, populating providers * asynchronously on background. * *

A client calling this method is responsible for passing * valid value that is accepted by attribute serving providers. * * @param key identifies target, never null * @param name identifies attribute, never null * @param value actual attribute value to be stored, null behaviour * is defined specifically for each name. It always invalidates memory entry * and it either invalidates or removes value from providers layer * (meaning: value unknown versus value is known to not exist). * *

Client should consider a size of stored value. It must be correlated * with Turbo memory layer limits to avoid running out of memory. * * @return

    *
  • false on write failure caused by a provider denying the value. * It means attribute contract violation and must be handled e.g.: *

    * boolean success = faq.writeAttribute(fo, name, value);
    * assert success : "Unexpected name[" + name + "] value[" + value + "] denial for " + key + "!"; *
    *

  • true in all other cases including I/O error. * After all it's just best effort cache. All values can be recomputed. *
*/ public boolean writeEntry(Object key, String name, Object value) { if (value != null) { Object oldValue = memory.get(key, name); if (oldValue != null && oldValue.equals(value)) return true; // XXX assuming provider has the same value, assert it! } int result = storeEntry(key, name, value); if (result >= 0) { // no one denied keep at least in memory cache memory.put(key, name, value); fireEntryChange(key, name, value); return true; } else { return false; } } /** * Stores directly to providers. * @return 0 success, -1 contract failure, 1 other failure */ int storeEntry(Object key, String name, Object value) { TurboProvider provider; Iterator it = providers(); while (it.hasNext()) { provider = (TurboProvider) it.next(); try { if (provider.recognizesAttribute(name) && provider.recognizesEntity(key)) { if (provider.writeEntry(key, name, value)) { return 0; } else { // for debugging purposes log which provider rejected defined name contract IllegalArgumentException ex = new IllegalArgumentException("Attribute[" + name + "] value rejected by " + provider); ErrorManager.getDefault().notify(ErrorManager.WARNING, ex); return -1; } } } catch (ThreadDeath td) { throw td; } catch (Throwable t) { // error in provider ErrorManager.getDefault().annotate(t, "Error in provider " + provider + ", skipping... "); // NOI18N ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t); } } return 1; } /** * Checks value instant availability and possibly schedules its background * loading. It's designed to be called from UI tread. * * @return
    *
  • false if not ready and providers must be consulted. It * asynchronously fires event possibly with null value * if given attribute does not exist. * *
  • * If true it's * ready and stays ready at least until next {@link #prepareEntry}, * {@link #isPrepared}, {@link #writeEntry} null call * or {@link #readEntry} from the same thread. *
*/ public boolean prepareEntry(Object key, String name) { statistics.attributeRequest(); // check memory cache if (memory.existsEntry(key, name)) { statistics.memoryHit(); return true; } // start asynchronous providers querying scheduleLoad(key, name); return false; } /** * Checks name instant availability. Note that actual * value may be still null, in case * that it's known that value does not exist. * * @return
    *
  • false if not present in memory for instant access. * *
  • true when it's * ready and stays ready at least until next {@link #prepareEntry}, * {@link #isPrepared}, {@link #writeEntry} null call * or {@link #readEntry} from the same thread. *
*/ public boolean isPrepared(Object key, String name) { return memory.existsEntry(key, name); } /** * Gets key instance that it actually used in memory layer. * Client should keep reference to it if it wants to use * key lifetime monitoring cache size strategy. * * @param key key never null * @return key instance that is value-equalent or null * if monitored instance does not exist. */ public Object getMonitoredKey(Object key) { return memory.getMonitoredKey(key); } public void addTurboListener(TurboListener l) { synchronized(listeners) { List copy = new ArrayList<>(listeners); copy.add(l); listeners = copy; } } public void removeTurboListener(TurboListener l) { synchronized(listeners) { List copy = new ArrayList<>(listeners); copy.remove(l); listeners = copy; } } protected void fireEntryChange(Object key, String name, Object value) { Iterator it = listeners.iterator(); while (it.hasNext()) { TurboListener next = (TurboListener) it.next(); next.entryChanged(key, name, value); } } /** For debugging purposes only. */ public String toString() { StringBuffer sb = new StringBuffer("Turbo delegating to:"); // NOI18N Iterator it = providers(); while (it.hasNext()) { TurboProvider provider = (TurboProvider) it.next(); sb.append(" [" + provider + "]"); // NOI18N } return sb.toString(); } /** Defines binding to external world. Used by tests. */ static class Environment { /** Lookup that serves providers. */ public Lookup getLookup() { return Lookup.getDefault(); } } // Background loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** Holds keys that were requested for background status retrieval. */ private final Set prepareRequests = Collections.synchronizedSet(new LinkedHashSet(27)); private static PreparationTask preparationTask; /** Tries to locate meta on disk on failure it forward to repository */ private void scheduleLoad(Object key, String name) { synchronized(prepareRequests) { if (preparationTask == null) { preparationTask = new PreparationTask(prepareRequests); Utils.postParallel(preparationTask, 0); statistics.backgroundThread(); } preparationTask.notifyNewRequest(new Request(key, name)); } } /** Requests queue entry featuring value based identity. */ private final static class Request { private final Object key; private final String name; public Request(Object key, String name) { this.name = name; this.key = key; } public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Request)) return false; final Request request = (Request) o; if (name != null ? !name.equals(request.name) : request.name != null) return false; if (key != null ? !key.equals(request.key) : request.key != null) return false; return true; } public int hashCode() { int result; result = (key != null ? key.hashCode() : 0); result = 29 * result + (name != null ? name.hashCode() : 0); return result; } public String toString() { return "Request[key=" + key + ", attr=" + name + "]"; } } /** * On background fetches data from providers layer. */ private final class PreparationTask implements Runnable { private final Set requests; private static final int INACTIVITY_TIMEOUT = 123 * 1000; // 123 sec public PreparationTask(Set requests) { this.requests = requests; } public void run() { try { Thread.currentThread().setName("Turbo Async Fetcher"); // NOI18N while (waitForRequests()) { Request request; synchronized (requests) { request = (Request) requests.iterator().next(); requests.remove(request); } Object key = request.key; String name = request.name; Object value; boolean fire; if (memory.existsEntry(key, name)) { synchronized(Memory.class) { fire = memory.existsEntry(key, name) == false; value = memory.get(key, name); } if (fire) { statistics.providerHit(); // from our perspective we achieved hit } } else { value = loadEntry(key, name, null); // possible thread switch, so atomic fire test must be used synchronized(Memory.class) { fire = memory.existsEntry(key, name) == false; Object oldValue = memory.get(key, name); memory.put(key, name, value != null ? value : Memory.NULL); fire |= (oldValue != null && !oldValue.equals(value)) || (oldValue == null && value != null); } } // some one was faster, probably previous disk read that silently fetched whole directory // our contract was to fire event once loading, stick to it. Note that get() // silently populates stable memory area // if (fire) { ALWAYS because of above loadAttribute(key, name, null); fireEntryChange(key, name, value); // notify as soon as available in memory // } } } catch (InterruptedException ex) { synchronized(requests) { // forget about recent requests requests.clear(); } } finally { synchronized(requests) { preparationTask = null; } } } /** * Wait for requests, it no request comes until timeout * it commits suicide. It's respawned on next request however. */ private boolean waitForRequests() throws InterruptedException { synchronized(requests) { if (requests.size() == 0) { requests.wait(INACTIVITY_TIMEOUT); } return requests.size() > 0; } } public void notifyNewRequest(Request request) { synchronized(requests) { if (requests.add(request)) { statistics.queueSize(requests.size()); requests.notify(); } else { statistics.duplicate(); statistics.providerHit(); } } } public String toString() { return "Turbo.PreparationTask queue=[" + requests +"]"; // NOI18N } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy