001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2023 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.Factory; 029import org.microbean.bean.ReferenceSelector; 030 031import org.microbean.qualifier.NamedAttributeMap; 032 033public abstract class MapBackedScopelet<M extends MapBackedScopelet<M>> extends Scopelet<M> { 034 035 private final ConcurrentMap<Object, Instance<?>> instances; 036 037 private final ConcurrentMap<Object, ReentrantLock> creationLocks; 038 039 protected MapBackedScopelet(final NamedAttributeMap<?> scopeId) { 040 super(scopeId); 041 this.creationLocks = new ConcurrentHashMap<>(); 042 this.instances = new ConcurrentHashMap<>(); 043 } 044 045 @Override // Scopelet<M> 046 public <I> I instance(final Object beanId, 047 final Factory<I> factory, 048 final Creation<I> c, 049 final ReferenceSelector r) { 050 if (!this.active()) { 051 throw new InactiveScopeletException(); 052 } 053 final Supplier<? extends I> s = this.supplier(beanId, factory, c, r); 054 return s == null ? null : s.get(); 055 } 056 057 private final <I> Supplier<I> supplier(final Object id, 058 final Factory<I> factory, 059 final Creation<I> creation, 060 final ReferenceSelector r) { 061 // (Don't use computeIfAbsent().) 062 @SuppressWarnings("unchecked") 063 final Supplier<I> supplier = (Supplier<I>)this.instances.get(id); 064 if (supplier != null || factory == null) { 065 // If we had a Supplier, return it, and if we didn't, and we have no means of creating a new one, return null. 066 return supplier; 067 } 068 069 // We can't use computeIfAbsent so things get a little tricky here. 070 // 071 // There wasn't anything in the instances map. So we want to effectively synchronize on instance creation. We're 072 // going to do this by maintaining Locks in a map, one per id in question. Please pay close attention to the 073 // locking semantics below. 074 075 // Create a new lock, but don't lock() it just yet. 076 final ReentrantLock newLock = new ReentrantLock(); 077 078 final ReentrantLock creationLock = this.creationLock(id, newLock); 079 assert creationLock.isLocked() : "!creationLock.isLocked(): " + creationLock; 080 081 if (creationLock == newLock) { 082 083 try { 084 085 // We successfully put newLock into the map. 086 assert this.creationLocks.get(id) == newLock; 087 088 // Perform creation. 089 @SuppressWarnings("unchecked") 090 final Instance<I> newInstance = 091 new Instance<>(factory == this ? (I)this : factory.create(creation, r), 092 factory::destroy, 093 creation, 094 r); 095 096 // Put the created instance into our instance map. There will not be a pre-existing instance. 097 final Object previous = this.instances.put(id, newInstance); 098 assert previous == null : "Unexpected prior instance: " + previous; 099 100 // Return the newly created instance. 101 return newInstance; 102 103 } finally { 104 try { 105 final Object removedLock = this.creationLocks.remove(id); 106 assert removedLock == newLock : "Unexpected removedLock: " + removedLock + "; newLock: " + newLock; 107 } finally { 108 newLock.unlock(); 109 } 110 } 111 } 112 113 // There was a Lock in the creationLocks map already that was not the newLock we just created. That's either 114 // another thread performing creation (an OK situation) or we have re-entered this method on the current thread 115 // and have encountered a newLock ancestor (probably not such a great situation). In any event, "our" newLock was 116 // never inserted into the map. It will therefore be unlocked (it was never locked in the first place). Discard 117 // it in preparation for switching locks to creationLock instead. 118 assert !newLock.isLocked() : "newLock was locked: " + newLock; 119 assert !this.creationLocks.containsValue(newLock) : "Creation lock contained " + newLock + "; creationLock: " + creationLock; 120 // Lock and unlock in rapid succession. Why? lock() will block if another thread is currently creating, and will 121 // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). 122 try { 123 creationLock.lock(); // potentially blocks 124 } finally { 125 creationLock.unlock(); 126 } 127 128 // If we legitimately blocked in the lock() operation, then another thread was truly creating, and it is 129 // guaranteed that thread will have removed the lock from our creationLocks map. If on the other hand the lock 130 // was held by this very thread, then no blocking occurred above, and we're in a cycle, and the lock will NOT have 131 // been removed from the creationLocks map (see above). Thus we can detect cycles by blindly attempting a 132 // removal: if it returns a non-null Object, we're in a cycle, otherwise everything is cool. Moreover, it is 133 // guaranteed that if the removal returns a non-null Object, that will be the creationLock this very thread 134 // inserted earlier. No matter what, we must make sure that the creationLocks map no longer contains a lock for 135 // the id in question. 136 final Object removedLock = this.creationLocks.remove(id); 137 if (removedLock != null) { 138 assert removedLock == creationLock : "Unexpected removedLock: " + removedLock + "; creationLock: " + creationLock; 139 throw new CreationCycleDetectedException(); 140 } 141 // The other thread finished creating; let's try again to pick up its results. 142 return this.supplier(id, factory, creation, r); // RECURSIVE 143 } 144 145 @Override // Scopelet<M> 146 public boolean remove(final Object id) { 147 if (!this.active()) { 148 throw new InactiveScopeletException(); 149 } 150 if (id != null) { 151 final Instance<?> instance = this.instances.remove(id); 152 if (instance != null) { 153 instance.close(); 154 return true; 155 } 156 } 157 return false; 158 } 159 160 @Override // Scopelet<M> 161 public void close() { 162 if (this.closed()) { 163 return; 164 } 165 super.close(); 166 final Iterator<Entry<Object, ReentrantLock>> i = this.creationLocks.entrySet().iterator(); 167 while (i.hasNext()) { 168 try { 169 i.next().getValue().unlock(); 170 } finally { 171 i.remove(); 172 } 173 } 174 final Iterator<Entry<Object, Instance<?>>> i2 = this.instances.entrySet().iterator(); 175 while (i2.hasNext()) { 176 try { 177 i2.next().getValue().close(); 178 } finally { 179 i2.remove(); 180 } 181 } 182 } 183 184 private final ReentrantLock creationLock(final Object id, final ReentrantLock candidate) { 185 if (candidate.isLocked()) { 186 throw new IllegalArgumentException("candidate.isLocked(): " + candidate); 187 } 188 try { 189 return this.creationLocks.computeIfAbsent(id, x -> lock(candidate)); 190 } catch (final RuntimeException | Error justBeingCareful) { 191 // ReentrantLock#lock() is not documented to throw anything, but if it does, make sure we unlock it. 192 try { 193 candidate.unlock(); 194 } catch (final RuntimeException | Error suppressMe) { 195 justBeingCareful.addSuppressed(suppressMe); 196 } 197 throw justBeingCareful; 198 } 199 } 200 201 private static final <T extends Lock> T lock(final T candidate) { 202 candidate.lock(); 203 return candidate; 204 } 205 206}