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.Creation; 028import org.microbean.bean.Destruction; 029import org.microbean.bean.Factory; 030 031/** 032 * A thread-safe, partial {@link Scopelet} implementation backed by {@link ConcurrentMap} machinery. 033 * 034 * @param <M> the {@link MapBackedScopelet} subclass extending this class 035 * 036 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 037 * 038 * @see #instance(Object, Factory, Creation) 039 */ 040public abstract class MapBackedScopelet<M extends MapBackedScopelet<M>> extends Scopelet<M> { 041 042 043 /* 044 * Instance fields. 045 */ 046 047 048 private final ConcurrentMap<Object, Instance<?>> instances; 049 050 private final ConcurrentMap<Object, ReentrantLock> creationLocks; 051 052 053 /* 054 * Constructors. 055 */ 056 057 058 /** 059 * Creates a new {@link MapBackedScopelet}. 060 * 061 * @see Scopelet#Scopelet() 062 */ 063 protected MapBackedScopelet() { 064 super(); 065 this.creationLocks = new ConcurrentHashMap<>(); 066 this.instances = new ConcurrentHashMap<>(); 067 } 068 069 070 /* 071 * Instance methods. 072 */ 073 074 075 @Override // Scopelet<M> 076 public void close() { 077 if (this.closed()) { 078 return; 079 } 080 super.close(); // critical 081 final Iterator<Entry<Object, ReentrantLock>> i = this.creationLocks.entrySet().iterator(); 082 while (i.hasNext()) { 083 final Entry<?, ? extends ReentrantLock> e = i.next(); 084 try { 085 e.getValue().unlock(); 086 } finally { 087 i.remove(); 088 } 089 } 090 final Iterator<Entry<Object, Instance<?>>> i2 = this.instances.entrySet().iterator(); 091 while (i2.hasNext()) { 092 final Entry<?, ? extends Instance<?>> e = i2.next(); 093 try { 094 e.getValue().close(); 095 } finally { 096 i2.remove(); 097 } 098 } 099 } 100 101 // All parameters are nullable. 102 @Override // Scopelet<M> 103 public <I> I instance(final Object beanId, final Factory<I> factory, final Creation<I> creation) { 104 if (!this.active()) { 105 throw new InactiveScopeletException(); 106 } else if (beanId == null) { 107 return null; 108 } 109 final Supplier<? extends I> s = this.supplier(beanId, factory, creation); 110 return s == null ? null : s.get(); 111 } 112 113 // If candidate is not present in this.creationLocks, puts it in in a locked state atomically and returns 114 // it. Otherwise returns the pre-existing creation lock, which, by definition, will already be locked. 115 private final ReentrantLock lockedCreationLock(final Object id, final ReentrantLock candidate) { 116 if (candidate.isLocked()) { 117 throw new IllegalArgumentException("candidate.isLocked(): " + candidate); 118 } 119 try { 120 return this.creationLocks.computeIfAbsent(id, x -> lock(candidate)); 121 } catch (final RuntimeException | Error justBeingCareful) { 122 // ReentrantLock#lock() is not documented to throw anything, but if it does, make sure we unlock it. 123 try { 124 candidate.unlock(); 125 } catch (final RuntimeException | Error suppressMe) { 126 justBeingCareful.addSuppressed(suppressMe); 127 } 128 throw justBeingCareful; 129 } 130 } 131 132 private final <I> Supplier<I> supplier(final Object id, 133 final Factory<I> factory, 134 final Creation<I> creation) { 135 // (Don't use computeIfAbsent().) 136 @SuppressWarnings("unchecked") 137 final Supplier<I> supplier = (Supplier<I>)this.instances.get(id); 138 if (supplier != null || factory == null) { 139 // If we had a Supplier, return it, and if we didn't, and we have no means of creating a new one, return null. 140 return supplier; 141 } 142 143 // We can't use computeIfAbsent so things get a little tricky here. 144 // 145 // There wasn't anything in the instances map. So we want to effectively synchronize on instance creation. We're 146 // going to do this by maintaining Locks in a map, one per id in question. Please pay close attention to the locking 147 // semantics below. 148 149 // Create a new lock, but don't lock() it just yet. 150 final ReentrantLock newLock = new ReentrantLock(); 151 152 // Atomically and gracefully lock it and put it into the creationLocks ConcurrentMap if there isn't one in there 153 // already. 154 final ReentrantLock creationLock = this.lockedCreationLock(id, newLock); 155 assert creationLock.isLocked() : "!creationLock.isLocked(): " + creationLock; 156 157 if (creationLock == newLock) { 158 159 // The creation lock is our new lock, which means there wasn't a pre-existing creation lock. Create. 160 161 try { 162 // (The finally block will unlock creationLock/newLock.) 163 164 // We successfully put newLock into the map so no creation was already in progress. 165 assert this.creationLocks.get(id) == newLock; 166 167 // Perform creation. 168 @SuppressWarnings("unchecked") 169 final Instance<I> newInstance = 170 new Instance<I>(factory == this ? (I)this : factory.create(creation), 171 factory::destroy, // Destructor 172 (Destruction)creation); 173 174 // Put the created instance into our instance map. There will not be a pre-existing instance. 175 final Object previous = this.instances.put(id, newInstance); 176 assert previous == null : "Unexpected prior instance: " + previous; 177 178 // Return the newly created instance. 179 return newInstance; 180 181 } finally { 182 try { 183 final Object removedLock = this.creationLocks.remove(id); 184 assert removedLock == newLock : "Unexpected removedLock: " + removedLock + "; newLock: " + newLock; 185 } finally { 186 newLock.unlock(); 187 } 188 } 189 } 190 191 // The creationLock was not our newLock. That means there was a Lock in the creationLocks map already that was not 192 // the newLock we just created. That's either another thread performing creation (an OK situation) or we have 193 // re-entered this method on the current thread and have encountered a newLock ancestor (probably not such a great 194 // situation). In any event, "our" newLock was never inserted into the map. It will therefore be unlocked (it was 195 // never locked in the first place). Discard it in preparation for switching locks to creationLock instead. 196 assert !newLock.isLocked() : "newLock was locked: " + newLock; 197 assert !this.creationLocks.containsValue(newLock) : 198 "Creation locks contained " + newLock + "; creationLock: " + creationLock; 199 // Lock and unlock in rapid succession. Why? lock() will block if another thread is currently creating, and will 200 // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). (Probably could use a 201 // Condition or a Semaphore here too. This is simple.) 202 try { 203 creationLock.lock(); // potentially blocks if creationLock is locked by another thread 204 } finally { 205 creationLock.unlock(); 206 } 207 208 // If we legitimately blocked in the lock() operation, then another thread was truly creating, and it is guaranteed 209 // that thread will have removed the lock from our creationLocks map. If on the other hand the lock was held by this 210 // very thread, then no blocking occurred above, and we're in a cycle, and the lock will NOT have been removed from 211 // the creationLocks map (see above). Thus we can detect cycles by blindly attempting a removal: if it returns a 212 // non-null Object, we're in a cycle, otherwise everything is cool. Moreover, it is guaranteed that if the removal 213 // returns a non-null Object, that will be the creationLock this very thread inserted earlier. No matter what, we 214 // must make sure that the creationLocks map no longer contains a lock for the id in question. 215 final Object removedLock = this.creationLocks.remove(id); 216 if (removedLock != null) { 217 assert removedLock == creationLock : "Unexpected removedLock: " + removedLock + "; creationLock: " + creationLock; 218 throw new CreationCycleDetectedException(); 219 } 220 // The other thread finished creating; let's try again to pick up its results. 221 return this.supplier(id, factory, creation); // RECURSIVE 222 } 223 224 @Override // Scopelet<M> 225 public boolean remove(final Object id) { 226 if (!this.active()) { 227 throw new InactiveScopeletException(); 228 } 229 if (id != null) { 230 final Instance<?> instance = this.instances.remove(id); 231 if (instance != null) { 232 instance.close(); 233 return true; 234 } 235 } 236 return false; 237 } 238 239 @Override // Scopelet<M> 240 public boolean removes() { 241 if (!this.active()) { 242 throw new InactiveScopeletException(); 243 } 244 return true; 245 } 246 247 248 /* 249 * Static methods. 250 */ 251 252 253 private static final <T extends Lock> T lock(final T candidate) { 254 candidate.lock(); 255 return candidate; 256 } 257 258}