package org.jruby.ext.thread_safe; import org.jruby.*; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.ext.thread_safe.jsr166e.ConcurrentHashMap; import org.jruby.ext.thread_safe.jsr166e.ConcurrentHashMapV8; import org.jruby.ext.thread_safe.jsr166e.nounsafe.*; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.load.Library; import java.io.IOException; import java.util.Map; import static org.jruby.runtime.Visibility.PRIVATE; /** * Native Java implementation to avoid the JI overhead. * * @author thedarkone */ public class JRubyCacheBackendLibrary implements Library { public void load(Ruby runtime, boolean wrap) throws IOException { RubyClass jrubyRefClass = runtime.defineClassUnder("JRubyCacheBackend", runtime.getObject(), BACKEND_ALLOCATOR, runtime.getModule("ThreadSafe")); jrubyRefClass.setAllocator(BACKEND_ALLOCATOR); jrubyRefClass.defineAnnotatedMethods(JRubyCacheBackend.class); } private static final ObjectAllocator BACKEND_ALLOCATOR = new ObjectAllocator() { public IRubyObject allocate(Ruby runtime, RubyClass klazz) { return new JRubyCacheBackend(runtime, klazz); } }; @JRubyClass(name="JRubyCacheBackend", parent="Object") public static class JRubyCacheBackend extends RubyObject { // Defaults used by the CHM static final int DEFAULT_INITIAL_CAPACITY = 16; static final float DEFAULT_LOAD_FACTOR = 0.75f; public static final boolean CAN_USE_UNSAFE_CHM = canUseUnsafeCHM(); private ConcurrentHashMap map; private static ConcurrentHashMap newCHM(int initialCapacity, float loadFactor) { if (CAN_USE_UNSAFE_CHM) { return new ConcurrentHashMapV8(initialCapacity, loadFactor); } else { return new org.jruby.ext.thread_safe.jsr166e.nounsafe.ConcurrentHashMapV8(initialCapacity, loadFactor); } } private static ConcurrentHashMap newCHM() { return newCHM(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } private static boolean canUseUnsafeCHM() { try { new org.jruby.ext.thread_safe.jsr166e.ConcurrentHashMapV8(); // force class load and initialization return true; } catch (Throwable t) { // ensuring we really do catch everything // Doug's Unsafe setup errors always have this "Could not ini.." message if (isCausedBySecurityException(t)) { return false; } throw (t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t)); } } private static boolean isCausedBySecurityException(Throwable t) { while (t != null) { if ((t.getMessage() != null && t.getMessage().contains("Could not initialize intrinsics")) || t instanceof SecurityException) { return true; } t = t.getCause(); } return false; } public JRubyCacheBackend(Ruby runtime, RubyClass klass) { super(runtime, klass); } @JRubyMethod public IRubyObject initialize(ThreadContext context) { map = newCHM(); return context.getRuntime().getNil(); } @JRubyMethod public IRubyObject initialize(ThreadContext context, IRubyObject options) { map = toCHM(context, options); return context.getRuntime().getNil(); } private ConcurrentHashMap toCHM(ThreadContext context, IRubyObject options) { Ruby runtime = context.getRuntime(); if (!options.isNil() && options.respondsTo("[]")) { IRubyObject rInitialCapacity = options.callMethod(context, "[]", runtime.newSymbol("initial_capacity")); IRubyObject rLoadFactor = options.callMethod(context, "[]", runtime.newSymbol("load_factor")); int initialCapacity = !rInitialCapacity.isNil() ? RubyNumeric.num2int(rInitialCapacity.convertToInteger()) : DEFAULT_INITIAL_CAPACITY; float loadFactor = !rLoadFactor.isNil() ? (float)RubyNumeric.num2dbl(rLoadFactor.convertToFloat()) : DEFAULT_LOAD_FACTOR; return newCHM(initialCapacity, loadFactor); } else { return newCHM(); } } @JRubyMethod(name = "[]", required = 1) public IRubyObject op_aref(ThreadContext context, IRubyObject key) { IRubyObject value; return ((value = map.get(key)) == null) ? context.getRuntime().getNil() : value; } @JRubyMethod(name = {"[]="}, required = 2) public IRubyObject op_aset(IRubyObject key, IRubyObject value) { map.put(key, value); return value; } @JRubyMethod public IRubyObject put_if_absent(IRubyObject key, IRubyObject value) { IRubyObject result = map.putIfAbsent(key, value); return result == null ? getRuntime().getNil() : result; } @JRubyMethod public IRubyObject compute_if_absent(final ThreadContext context, final IRubyObject key, final Block block) { return map.computeIfAbsent(key, new ConcurrentHashMap.Fun() { @Override public IRubyObject apply(IRubyObject key) { return block.yieldSpecific(context); } }); } @JRubyMethod public IRubyObject compute_if_present(final ThreadContext context, final IRubyObject key, final Block block) { IRubyObject result = map.computeIfPresent(key, new ConcurrentHashMap.BiFun() { @Override public IRubyObject apply(IRubyObject key, IRubyObject oldValue) { IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue); return result.isNil() ? null : result; } }); return result == null ? context.getRuntime().getNil() : result; } @JRubyMethod public IRubyObject compute(final ThreadContext context, final IRubyObject key, final Block block) { IRubyObject result = map.compute(key, new ConcurrentHashMap.BiFun() { @Override public IRubyObject apply(IRubyObject key, IRubyObject oldValue) { IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue); return result.isNil() ? null : result; } }); return result == null ? context.getRuntime().getNil() : result; } @JRubyMethod public IRubyObject merge_pair(final ThreadContext context, final IRubyObject key, final IRubyObject value, final Block block) { IRubyObject result = map.merge(key, value, new ConcurrentHashMap.BiFun() { @Override public IRubyObject apply(IRubyObject oldValue, IRubyObject newValue) { IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue); return result.isNil() ? null : result; } }); return result == null ? context.getRuntime().getNil() : result; } @JRubyMethod public RubyBoolean replace_pair(IRubyObject key, IRubyObject oldValue, IRubyObject newValue) { return getRuntime().newBoolean(map.replace(key, oldValue, newValue)); } @JRubyMethod(name = "key?", required = 1) public RubyBoolean has_key_p(IRubyObject key) { return map.containsKey(key) ? getRuntime().getTrue() : getRuntime().getFalse(); } @JRubyMethod public IRubyObject key(IRubyObject value) { final IRubyObject key = map.findKey(value); return key == null ? getRuntime().getNil() : key; } @JRubyMethod public IRubyObject replace_if_exists(IRubyObject key, IRubyObject value) { IRubyObject result = map.replace(key, value); return result == null ? getRuntime().getNil() : result; } @JRubyMethod public IRubyObject get_and_set(IRubyObject key, IRubyObject value) { IRubyObject result = map.put(key, value); return result == null ? getRuntime().getNil() : result; } @JRubyMethod public IRubyObject delete(IRubyObject key) { IRubyObject result = map.remove(key); return result == null ? getRuntime().getNil() : result; } @JRubyMethod public RubyBoolean delete_pair(IRubyObject key, IRubyObject value) { return getRuntime().newBoolean(map.remove(key, value)); } @JRubyMethod public IRubyObject clear() { map.clear(); return this; } @JRubyMethod public IRubyObject each_pair(ThreadContext context, Block block) { for (Map.Entry entry : map.entrySet()) { block.yieldSpecific(context, entry.getKey(), entry.getValue()); } return this; } @JRubyMethod public RubyFixnum size(ThreadContext context) { return context.getRuntime().newFixnum(map.size()); } @JRubyMethod public IRubyObject get_or_default(IRubyObject key, IRubyObject defaultValue) { return map.getValueOrDefault(key, defaultValue); } @JRubyMethod(visibility = PRIVATE) public JRubyCacheBackend initialize_copy(ThreadContext context, IRubyObject other) { map = newCHM(); return this; } } }