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}