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 try { 160 // (The finally block will unlock creationLock/newLock.) 161 162 // We successfully put newLock into the map so no creation was already in progress. 163 assert this.creationLocks.get(id) == newLock; 164 165 // Perform creation. 166 @SuppressWarnings("unchecked") 167 final Instance<I> newInstance = 168 new Instance<I>(factory == this ? (I)this : factory.create(creation), 169 factory::destroy, // Destructor 170 (Destruction)creation); 171 172 // Put the created instance into our instance map. There will not be a pre-existing instance. 173 final Object previous = this.instances.put(id, newInstance); 174 assert previous == null : "Unexpected prior instance: " + previous; 175 176 // Return the newly created instance. 177 return newInstance; 178 179 } finally { 180 try { 181 final Object removedLock = this.creationLocks.remove(id); 182 assert removedLock == newLock : "Unexpected removedLock: " + removedLock + "; newLock: " + newLock; 183 } finally { 184 newLock.unlock(); 185 } 186 } 187 } 188 189 // There was a Lock in the creationLocks map already that was not the newLock we just created. That's either another 190 // thread performing creation (an OK situation) or we have re-entered this method on the current thread and have 191 // encountered a newLock ancestor (probably not such a great situation). In any event, "our" newLock was never 192 // inserted into the map. It will therefore be unlocked (it was never locked in the first place). Discard it in 193 // preparation for switching locks to creationLock instead. 194 assert !newLock.isLocked() : "newLock was locked: " + newLock; 195 assert !this.creationLocks.containsValue(newLock) : 196 "Creation locks contained " + newLock + "; creationLock: " + creationLock; 197 // Lock and unlock in rapid succession. Why? lock() will block if another thread is currently creating, and will 198 // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). 199 try { 200 creationLock.lock(); // potentially blocks 201 } finally { 202 creationLock.unlock(); 203 } 204 205 // If we legitimately blocked in the lock() operation, then another thread was truly creating, and it is guaranteed 206 // that thread will have removed the lock from our creationLocks map. If on the other hand the lock was held by this 207 // very thread, then no blocking occurred above, and we're in a cycle, and the lock will NOT have been removed from 208 // the creationLocks map (see above). Thus we can detect cycles by blindly attempting a removal: if it returns a 209 // non-null Object, we're in a cycle, otherwise everything is cool. Moreover, it is guaranteed that if the removal 210 // returns a non-null Object, that will be the creationLock this very thread inserted earlier. No matter what, we 211 // must make sure that the creationLocks map no longer contains a lock for the id in question. 212 final Object removedLock = this.creationLocks.remove(id); 213 if (removedLock != null) { 214 assert removedLock == creationLock : "Unexpected removedLock: " + removedLock + "; creationLock: " + creationLock; 215 throw new CreationCycleDetectedException(); 216 } 217 // The other thread finished creating; let's try again to pick up its results. 218 return this.supplier(id, factory, creation); // RECURSIVE 219 } 220 221 @Override // Scopelet<M> 222 public boolean remove(final Object id) { 223 if (!this.active()) { 224 throw new InactiveScopeletException(); 225 } 226 if (id != null) { 227 final Instance<?> instance = this.instances.remove(id); 228 if (instance != null) { 229 instance.close(); 230 return true; 231 } 232 } 233 return false; 234 } 235 236 237 /* 238 * Static methods. 239 */ 240 241 242 private static final <T extends Lock> T lock(final T candidate) { 243 candidate.lock(); 244 return candidate; 245 } 246 247}