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}