Лучший шаблон проектирования для управления асимметричным использованием ресурсов

Я хотел, чтобы на холсте были высказаны мнения о наилучшем шаблоне проектирования для работы с управляемыми ресурсами, в котором задействованы два разных ресурса, но вам нужно освободить их в обратном порядке, чем они были приобретены.

Сначала позвольте мне установить сцену. Мы работаем с двумя типами объектов "Документы" и "Коллекции документов". Сборник документов буквально содержит ссылки на документы и некоторые метаданные для каждого документа.

Первоначально у нас был симметричный рисунок, который протекал как:

  • Коллекция блокировок
  • Полезный материал с коллекцией
  • Заблокировать документ
  • Полезный материал с коллекцией и документом
  • Разблокировать документ
  • Разблокировать коллекцию

и в коде был представлен как:

Collection col = null;
try {
    col = getCollection("col1 name", LockMode.WRITE_LOCK);

    // Here we do any operations that only require the Collection

    Document doc = null;
    try {
        doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);

        // Here we do some operations on the document (of the Collection)

    } finally {
        if (doc != null) {
            doc.close();
        }
    }

} finally {
    if (col != null) {
        col.close();
    }
}

Теперь, когда мы имеем try-with-resources с Java 7, мы улучшили это, чтобы разметка кода Java автоматически освобождала ресурсы:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {

    // Here we do any operations that only require the Collection

    try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {

        // Here we do some operations on the document (of the Collection)

    }

}

Проблема заключается в том, что сохранение блокировки коллекции во время выполнения операций над документом неэффективно, так как другие потоки должны ждать, и часто операции над документом не требуют изменения коллекции.

Итак, мы хотели бы перейти к асимметричной схеме, которая позволяет нам как можно скорее выпустить коллекцию. Поток должен быть следующим:

  • Коллекция блокировок
  • Полезный материал с коллекцией
  • Заблокировать документ
  • Делайте все, что требует как Collection, так и Document (редко)
  • Разблокировать коллекцию
  • Полезные материалы с документом
  • Разблокировать документ

Мне интересно, как лучше всего реализовать этот асимметричный подход в коде. Это, очевидно, можно сделать с помощью try/finally и т.д. Так:

Collection col = null;
Document doc = null;
try {
    col = getCollection("col1 name", LockMode.WRITE_LOCK);

    // Here we do any operations that only require the Collection
    try {
        doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK);

        // Here we do any operations that require both the Collection and Document (rare).

    } finally {
        if (col != null) {
        col.close();
    }

    // Here we do some operations on the document (of the Collection)

} finally {
    if (doc != null) {
            doc.close();
        }
    }
}

Я также могу подумать о схеме try-with-resources, где мы обмениваемся порядком выпуска ресурсов, но мне интересно, не делает ли это чтение кода менее понятным. Например:

try (final ManagedRelease<Collection> mcol =
        new ManagedRelease<>(getCollection("col1 name", LockMode.WRITE_LOCK))) {

    // Here we do any operations that only require the Collection

    try (final ManagedRelease<Document> mdoc =
            mcol.withAsymetrical(mcol.resource.getDocument("doc1 name", LockMode.WRITE_LOCK))) {

        // Here we do any operations that require both the Collection and Document (rare).

    }  // NOTE: Collection is released here

    // Here we do some operations on the document (of the Collection)

}  // NOTE: Document is released here

Класс ManagedRelease:

private static class ManagedRelease<T extends AutoCloseable> implements AutoCloseable {
    final T resource;
    private Supplier<Optional<Exception>> closer;

    public ManagedRelease(final T resource) {
        this.resource = resource;
        this.closer = asCloserFn(resource);
    }

    private ManagedRelease(final T resource, final Supplier<Optional<Exception>> closer) {
        this.resource = resource;
        this.closer = closer;
    }

    public <U extends AutoCloseable> ManagedRelease<U> withAsymetrical(final U otherResource) {
        // switch the closers of ManagedRelease<T> and ManagedRelease<U>
        final ManagedRelease<U> asymManagedResource = new ManagedRelease<>(otherResource, closer);
        this.closer = asCloserFn(otherResource);
        return asymManagedResource;
    }

    @Override
    public void close() throws Exception {
        final Optional<Exception> maybeEx = closer.get();
        if(maybeEx.isPresent()) {
            throw maybeEx.get();
        }
    }

    private static Supplier<Optional<Exception>> asCloserFn(final AutoCloseable autoCloseable) {
        return () -> {
            try {
                autoCloseable.close();
                return Optional.empty();
            } catch (final Exception e) {
                return Optional.of(e);
            }
        };
    }
}

Я бы приветствовал мнения о том, является ли подход try-with-resources асимметричным управлением ресурсами разумным или нет, а также любые указатели на другие шаблоны, которые могут быть более подходящими.

Ответ 1

Первый вопрос, по-видимому, является недоопределенным ожидаемым поведением. В частности, если Collection.close выбрасывает Exception, что должно произойти? Должна ли продолжаться обработка Document? Следует ли откатить часть обработки документа, сделанного под обоими замками?

Если ответ Collection.close никогда не выдает никаких исключений (или вам все равно, что произойдет, если это произойдет), IMHO - самое простое решение сделать ваш idmpotent Collection.close, а затем явно вызвать его в середине блок try-with-resources, где это уместно. Также имеет смысл заставить "обычные" методы Collection поднять что-то вроде IllegalStateException, если вызывается в закрытом Collection. Тогда второй пример станет примерно таким:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {
    // Here we do any operations that only require the Collection

    try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {

        // Here we do any operations that require both the Collection and Document (rare).


        // NOTE: usually Collection is released here
        col.close();
        // optionally make `col` not final and explicitly set it to `null`
        // here so IDE would notify you about any usage after this point

        // Here we do some operations on the document (of the Collection)

    }  
}  

Если вы не можете изменить код Collection.close, вы можете изменить свой ReleaseManager, чтобы сделать close idempotent. Возможно, вы также можете переименовать его в нечто вроде ResourceManager. добавьте получателя туда и всегда получайте доступ к ресурсу только через этот getter. И геттер будет бросать IllegalStateException, если вызывается после close.

Если Collection.close может действительно вызвать какое-то исключение, и вы действительно заботитесь о таких сценариях, трудно предоставить решение, не зная, что такое ожидаемое поведение.

Ответ 2

Я дам вам общее, полное и цельное решение, подобное этому:

   public static void sample() {
    Resource resourceA = new Resource("A");
    Resource resourceB = new Resource("B");
    LockVisitor.create(resourceA)
        .lock()// lock A
        .doOnValue(Main::doSomething)// do for A
        .with(resourceB)// join with B
        .lock()// lock A & B (A has been locked)
        .doOnBoth(Main::doSomething)// do for A and B
        .toRight()// only need B (unlock A)
        .doOnValue(Main::doSomething)// do for B
        .close();// unlock B
  }

  private static void doSomething(Resource... rs) {
    System.out.println("do with: " + Arrays.toString(rs));
  }

и sample выводят, что вы ожидали:

lock: Resource(A)
do with: [Resource(A)]
lock: Resource(B)
do with: [Resource(A), Resource(B)]
unlock: Resource(A)
do with: [Resource(B)]
unlock: Resource(B)

Сначала мы должны определить блокируемый ресурс. Как заблокировать и как разблокировать.

public interface Lockable extends AutoCloseable {

  void lock() throws Exception;

  void unlock() throws Exception;

  boolean isLocked();

  @Override
  default void close() throws Exception {
    unlock();
  }
}

Вы можете позволить вашему классу реализовать этот интерфейс для более четкого вызова.

Затем мы можем построить наш LockVisitor (для уменьшения длины этого ответа я удаляю реализацию метода. Вы можете найти полный код в github.)

import io.reactivex.functions.Consumer;

public class LockVisitor<T extends Lockable> implements AutoCloseable {
  public static <T extends Lockable> LockVisitor<T> create(T lockable) {
    return new LockVisitor<>(lockable);
  }

  T value;
  Exception error;

  public LockVisitor(T value);

  public LockVisitor<T> lock();

  public LockVisitor<T> unlock();

  public LockVisitor<T> doOnValue(Consumer<T> func);

  public LockVisitor<T> doOnError(Consumer<Exception> func);

  public <B extends Lockable> TwoLockVisitor<T, B> with(LockVisitor<B> other);

  public <B extends Lockable> TwoLockVisitor<T, B> with(B other);
}

и наш TwoLockVisitor для совместного использования двух ресурсов:

import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;

public class TwoLockVisitor<A extends Lockable, B extends Lockable> {
  public static <A extends Lockable, B extends Lockable> TwoLockVisitor<A, B> create(A a, B b) {
    return new TwoLockVisitor<>(LockVisitor.create(a), LockVisitor.create(b));
  }

  LockVisitor<A> left;
  LockVisitor<B> right;

  public TwoLockVisitor(LockVisitor<A> left, LockVisitor<B> right);

  public TwoLockVisitor<A, B> lock();

  public TwoLockVisitor<A, B> unlock();

  public TwoLockVisitor<A, B> doOnLeft(Consumer<A> func);

  public TwoLockVisitor<A, B> doOnRight(Consumer<B> func);

  public TwoLockVisitor<A, B> doOnBoth(BiConsumer<A, B> func);

  public LockVisitor<A> toLeft();

  public LockVisitor<B> toRight();
}

Теперь вы можете использовать классы для управления вашим ресурсом в любом порядке.

Ответ 3

Ваша схема ManagedRelease определенно делает код менее понятным. Самая непосредственная явная запись ваших намерений с использованием языковых функций выглядит следующим образом:

try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK)) {

    // Here we do any operations that only require the Collection

}
try (final Collection col = getCollection("col1 name", LockMode.WRITE_LOCK;
    final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {

    // Here we do any operations that require both the Collection and Document (rare).

}
try (final Document doc = col.getDocument("doc1 name", LockMode.WRITE_LOCK)) {

    // Here we do some operations on the document (of the Collection)

}

Проблема заключается в дополнительном выпуске и повторном приобретении каждой блокировки, а также что col выходит за пределы области для последнего вызова getDocument, поэтому он не будет компилироваться как есть.

Я бы предложил разрешить это с другой точки зрения по концепции ManagedRelease, поднятой на один уровень. Схема использования, которую я предполагаю для этого, будет работать следующим образом:

// The lambdas here are Supplier
try (final ReleaseManager<Collection> colManager = new ReleaseManager<>(() -> getCollection("col1 name", LockMode.WRITE_LOCK);
    final ReleaseManager<Document> docManager = new ReleaseManager<>(() -> colManager.getResource().get().getDocument("doc1 name", LockMode.WRITE_LOCK)) {

    try (final Managed<Collection> colManaged = colManager.getResource()) {

        // Here we do any operations that only require the Collection

    } // Here the resource close does nothing

    try (final Managed<Collection> colManaged = colManager.getResourceForLastUse();
        final Managed<Document> docManaged = docManager.getResource()) {

        // Here we do any operations that require both the Collection and Document (rare).

    } // Here the close of colManaged actually closes it, while docManaged.close() is a no-op

    try (final Managed<Document> docManaged = docManager.getResourceForLastUse()) {

        // Here we do some operations on the document (of the Collection)

    } // Here the document gets closed
} // Here the managers get closed, which would close their resources if needed

Это имеет ту же ясность, что ресурсы используются в каждом блоке, использует функцию языка try-with-resources, выпускает каждый ресурс сразу после его последнего использования и только получает каждую блокировку один раз.

Для спецификации ReleaseManager:

ReleaseManager вот общий класс, который берет Supplier для ресурса, лениво вызывает его при первом вызове getResource() и запоминает результат для будущих вызовов. getResource() возвращает оболочку, которая ничего не делает при закрытии, getResourceForLastUse() возвращает оболочку, которая фактически закрывает ресурс, когда оболочка закрыта; Я написал их как один класс, но вместо этого вы могли бы сделать их разными классами, я не уверен, действительно ли он делает что-то более ясное.

ReleaseManager сам реализует AutoCloseable, а его реализация close() является отказоустойчивой, которая закрывает ресурс, если он был получен, но не закрыт. Я хотел бы подумать о том, чтобы он также каким-то образом зарегистрировал предупреждение, чтобы привлечь внимание, если последнее использование ресурса не будет объявлено должным образом последним. И для одного окончательного рассмотрения оба метода поиска ресурсов должны бросать, если ресурс уже закрыт.

Я оставляю реализацию ReleaseManager как упражнение для вас, если вам нравится это решение.