001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2023–2025 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 012 * specific language governing permissions and limitations under the License. 013 */ 014package org.microbean.scopelet; 015 016import java.util.Iterator; 017import java.util.Map.Entry; 018 019import java.util.concurrent.ConcurrentHashMap; 020import java.util.concurrent.ConcurrentMap; 021 022import java.util.concurrent.locks.Lock; 023import java.util.concurrent.locks.ReentrantLock; 024 025import java.util.function.Supplier; 026 027import org.microbean.bean.Factory; 028import org.microbean.bean.Request; 029 030import org.microbean.qualifier.NamedAttributeMap; 031 032/** 033 * A thread-safe, partial {@link Scopelet} implementation backed by {@link ConcurrentMap} machinery. 034 * 035 * @param <M> the {@link MapBackedScopelet} subclass extending this class 036 * 037 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 038 */ 039public abstract class MapBackedScopelet<M extends MapBackedScopelet<M>> extends Scopelet<M> { 040 041 private final ConcurrentMap<Object, Instance<?>> instances; 042 043 private final ConcurrentMap<Object, ReentrantLock> creationLocks; 044 045 /** 046 * Creates a new {@link MapBackedScopelet}. 047 * 048 * @param scopeId a {@link NamedAttributeMap} identifying the scope this {@link MapBackedScopelet} serves; must not be 049 * {@code null} 050 * 051 * @exception NullPointerException if {@code scopeId} is {@code null} 052 * 053 * @see Scopelet#Scopelet(NamedAttributeMap) 054 */ 055 protected MapBackedScopelet(final NamedAttributeMap<?> scopeId) { 056 super(scopeId); 057 this.creationLocks = new ConcurrentHashMap<>(); 058 this.instances = new ConcurrentHashMap<>(); 059 } 060 061 // All parameters are nullable. 062 @Override // Scopelet<M> 063 public <I> I instance(final Object beanId, 064 final Factory<I> factory, 065 final Request<I> request) { 066 if (!this.active()) { 067 throw new InactiveScopeletException(); 068 } else if (beanId == null) { 069 return null; 070 } 071 final Supplier<? extends I> s = this.supplier(beanId, factory, request); 072 return s == null ? null : s.get(); 073 } 074 075 private final <I> Supplier<I> supplier(final Object id, 076 final Factory<I> factory, 077 final Request<I> request) { 078 // (Don't use computeIfAbsent().) 079 @SuppressWarnings("unchecked") 080 final Supplier<I> supplier = (Supplier<I>)this.instances.get(id); 081 if (supplier != null || factory == null) { 082 // If we had a Supplier, return it, and if we didn't, and we have no means of creating a new one, return null. 083 return supplier; 084 } 085 086 // We can't use computeIfAbsent so things get a little tricky here. 087 // 088 // There wasn't anything in the instances map. So we want to effectively synchronize on instance creation. We're 089 // going to do this by maintaining Locks in a map, one per id in question. Please pay close attention to the locking 090 // semantics below. 091 092 // Create a new lock, but don't lock() it just yet. 093 final ReentrantLock newLock = new ReentrantLock(); 094 095 // Atomically and gracefully lock it and put it into the creationLocks ConcurrentMap if there isn't one in there 096 // already. 097 final ReentrantLock creationLock = this.lockedCreationLock(id, newLock); 098 assert creationLock.isLocked() : "!creationLock.isLocked(): " + creationLock; 099 100 if (creationLock == newLock) { 101 102 try { 103 // (The finally block will unlock creationLock/newLock.) 104 105 // We successfully put newLock into the map so no creation was already in progress. 106 assert this.creationLocks.get(id) == newLock; 107 108 // Perform creation. 109 @SuppressWarnings("unchecked") 110 final Instance<I> newInstance = 111 new Instance<I>(factory == this ? (I)this : factory.create(request), 112 factory::destroy, // Destructor 113 request); 114 115 // Put the created instance into our instance map. There will not be a pre-existing instance. 116 final Object previous = this.instances.put(id, newInstance); 117 assert previous == null : "Unexpected prior instance: " + previous; 118 119 // Return the newly created instance. 120 return newInstance; 121 122 } finally { 123 try { 124 final Object removedLock = this.creationLocks.remove(id); 125 assert removedLock == newLock : "Unexpected removedLock: " + removedLock + "; newLock: " + newLock; 126 } finally { 127 newLock.unlock(); 128 } 129 } 130 } 131 132 // There was a Lock in the creationLocks map already that was not the newLock we just created. That's either another 133 // thread performing creation (an OK situation) or we have re-entered this method on the current thread and have 134 // encountered a newLock ancestor (probably not such a great situation). In any event, "our" newLock was never 135 // inserted into the map. It will therefore be unlocked (it was never locked in the first place). Discard it in 136 // preparation for switching locks to creationLock instead. 137 assert !newLock.isLocked() : "newLock was locked: " + newLock; 138 assert !this.creationLocks.containsValue(newLock) : "Creation lock contained " + newLock + "; creationLock: " + creationLock; 139 // Lock and unlock in rapid succession. Why? lock() will block if another thread is currently creating, and will 140 // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). 141 try { 142 creationLock.lock(); // potentially blocks 143 } finally { 144 creationLock.unlock(); 145 } 146 147 // If we legitimately blocked in the lock() operation, then another thread was truly creating, and it is guaranteed 148 // that thread will have removed the lock from our creationLocks map. If on the other hand the lock was held by this 149 // very thread, then no blocking occurred above, and we're in a cycle, and the lock will NOT have been removed from 150 // the creationLocks map (see above). Thus we can detect cycles by blindly attempting a removal: if it returns a 151 // non-null Object, we're in a cycle, otherwise everything is cool. Moreover, it is guaranteed that if the removal 152 // returns a non-null Object, that will be the creationLock this very thread inserted earlier. No matter what, we 153 // must make sure that the creationLocks map no longer contains a lock for the id in question. 154 final Object removedLock = this.creationLocks.remove(id); 155 if (removedLock != null) { 156 assert removedLock == creationLock : "Unexpected removedLock: " + removedLock + "; creationLock: " + creationLock; 157 throw new CreationCycleDetectedException(); 158 } 159 // The other thread finished creating; let's try again to pick up its results. 160 return this.supplier(id, factory, request); // RECURSIVE 161 } 162 163 @Override // Scopelet<M> 164 public boolean remove(final Object id) { 165 if (!this.active()) { 166 throw new InactiveScopeletException(); 167 } 168 if (id != null) { 169 final Instance<?> instance = this.instances.remove(id); 170 if (instance != null) { 171 instance.close(); 172 return true; 173 } 174 } 175 return false; 176 } 177 178 @Override // Scopelet<M> 179 public void close() { 180 if (this.closed()) { 181 return; 182 } 183 super.close(); // critical 184 final Iterator<Entry<Object, ReentrantLock>> i = this.creationLocks.entrySet().iterator(); 185 while (i.hasNext()) { 186 final Entry<?, ? extends ReentrantLock> e = i.next(); 187 try { 188 e.getValue().unlock(); 189 } finally { 190 i.remove(); 191 } 192 } 193 final Iterator<Entry<Object, Instance<?>>> i2 = this.instances.entrySet().iterator(); 194 while (i2.hasNext()) { 195 final Entry<?, ? extends Instance<?>> e = i2.next(); 196 try { 197 e.getValue().close(); 198 } finally { 199 i2.remove(); 200 } 201 } 202 } 203 204 // If candidate is not present in this.creationLocks, puts it in in a locked state atomically and returns 205 // it. Otherwise returns the pre-existing creation lock, which, by definition, will already be locked. 206 private final ReentrantLock lockedCreationLock(final Object id, final ReentrantLock candidate) { 207 if (candidate.isLocked()) { 208 throw new IllegalArgumentException("candidate.isLocked(): " + candidate); 209 } 210 try { 211 return this.creationLocks.computeIfAbsent(id, x -> lock(candidate)); 212 } catch (final RuntimeException | Error justBeingCareful) { 213 // ReentrantLock#lock() is not documented to throw anything, but if it does, make sure we unlock it. 214 try { 215 candidate.unlock(); 216 } catch (final RuntimeException | Error suppressMe) { 217 justBeingCareful.addSuppressed(suppressMe); 218 } 219 throw justBeingCareful; 220 } 221 } 222 223 private static final <T extends Lock> T lock(final T candidate) { 224 candidate.lock(); 225 return candidate; 226 } 227 228}