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}