Как отменить будущее действие, если другое будущее не удалось? - PullRequest
3 голосов
/ 17 июня 2020

У меня есть 2 фьючерса (2 действия в таблицах db), и я хочу, чтобы перед сохранением изменений проверяли, успешно ли завершились оба фьючерса. ), но знаю, что это не лучший вариант. Я знаю, что могу использовать for -понимание для параллельного выполнения обоих фьючерсов, но даже если один из них не сработает, другой будет выполнен (еще не протестирован)

firstFuture.dropColumn(tableName) match {
  case Success(_) => secondFuture.deleteEntity(entity)
  case Failure(e) => throw new Exception(e.getMessage)
}

// the  first future alters a table, drops a column
// the second future deletes a row from another table

в этом случае, если первый в будущем выполняется успешно, второй может выйти из строя. Я хочу откатить обновление first future. Я слышал о SQL транзакциях, вроде бы что-то в этом роде, но как?

val futuresResult = for {
  first <- firstFuture.dropColumn(tableName)
  second <- secondFuture.deleteEntity(entity)
} yield (first, second)

A for - понимание намного лучше в моем случае, потому что у меня нет зависимостей между этими двумя фьючерсами и может выполняться параллельно, но это не решает мою проблему, результатом может быть, например, (успех, успех) или (неудача, успех).

1 Ответ

9 голосов
/ 17 июня 2020

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

Это немного сложно, потому что Scala Future спроектирован как нетерпеливый . В различных библиотеках Scala есть некоторые другие конструкции, которые обрабатывают синхронные и асинхронные эффекты, например, cats IO, Monix Task, ZIO et c. которые разработаны по принципу ленивого , и у них нет такого поведения.

Дело в том, что Future стремится к тому, что он начнет вычисление, как только сможет . Здесь «начало» означает планирование его на ExecutionContext, который либо выбран явно, либо присутствует неявно. Хотя технически возможно, что выполнение немного остановится в случае, если планировщик решит сделать это, скорее всего, он будет запущен почти мгновенно.

Итак, если у вас есть значение типа Future, оно будет начать бегать тут же. Если у вас есть ленивое значение типа Future или функция / метод, возвращающий значение типа Future, тогда это не так.

Но даже если у вас есть простые значения (нет ленивых значений или defs), если определение Future выполняется внутри for-computing, то это означает, что оно является частью цепочки monadi c flatMap (если вы этого не понимаете, игнорируйте его пока), и оно будет запущено последовательно , а не параллельно. Зачем? Это не указано с c по Future s; каждое понимание for имеет семантику последовательной цепочки, в которой вы можете передать результат предыдущего шага следующему шагу. Поэтому логично, что вы не можете запустить что-то на шаге n + 1 , если это зависит от чего-то из шага n .

Вот какой-то код, чтобы продемонстрировать это.

val program = for {
  _ <- Future { Thread.sleep(5000); println("f1") }
  _ <- Future { Thread.sleep(5000); println("f2") }
} yield ()

Await.result(program, Duration.Inf)

Эта программа подождет пять секунд, затем напечатает «f1», затем подождет еще пять секунд и затем напечатает «f2».

Теперь давайте посмотрим на это:

val f1 = Future { Thread.sleep(5000); println("f1") }
val f2 = Future { Thread.sleep(5000); println("f2") }

val program = for {
  _ <- f1
  _ <- f2
} yield ()

Await.result(program, Duration.Inf)

Однако программа будет печатать «f1» и «f2» одновременно через пять секунд.

Обратите внимание, что во втором случае семантика последовательности на самом деле не нарушается. f2 по-прежнему имеет возможность использовать результат f1. Но f2 не использует результат f1; это отдельное значение, которое может быть вычислено немедленно (определяется с помощью val). Итак, если мы заменим val f2 на функцию, например def f2(number: Int), то выполнение изменится:

val f1 = Future { Thread.sleep(5000); println("f1"); 42 }
def f2(number: Int) = Future { Thread.sleep(5000); println(number) }

val program = for {
  number <- f1
  _ <- f2(number)
} yield ()

Как и следовало ожидать, через пять секунд будет выведено «f1», и только тогда other Future start, поэтому он напечатает «42» еще через пять секунд.

Что касается транзакций:

Как @cbley упомянул в комментарии, это звучит так, будто вам нужны транзакции базы данных. Например, в базах данных SQL это имеет очень специфику c, означающую , и обеспечивает свойства ACID .

Если это то, что вам нужно, вам нужно решить это на уровне базы данных. Future слишком общий c для этого; это просто тип эффекта, моделирующий вычисления syn c и asyn c. Когда вы видите значение Future, просто взглянув на тип, вы не можете определить, является ли это результатом вызова базы данных или, скажем, какого-то HTTP-вызова.

Например, doob ie описывает каждый запрос к базе данных как тип ConnectionIO. У вас может быть несколько запросов, выстроенных в линию для понимания, как в случае с Future:

val program = for {
  a <- database.getA()
  _ <- database.write("foo")
  b <- database.getB()
} yield {
  // use a and b
}

Но в отличие от наших предыдущих примеров, здесь getA() и getB() не возвращаются значение типа Future[A], но ConnectionIO[A]. Что круто в этом, так это то, что doob ie полностью заботится о том, что вы, вероятно, хотите, чтобы эти запросы выполнялись в одной транзакции, поэтому, если getB() завершится неудачно, "foo" не будет зафиксировано в базе данных. *

В этом случае вы должны получить полное описание вашего набора запросов, заключить его в одно значение program типа ConnectionIO, и как только вы действительно захотите запустить транзакцию, вы должны сделайте что-нибудь вроде program.transact(myTransactor), где myTransactor - это экземпляр Transactor, doob ie конструкция , которая знает, как подключиться к вашей физической базе данных.

И как только вы совершаете транзакцию, ваш ConnectionIO[A] превращается в Future[A]. Если транзакция не удалась, вы получите ошибку Future, и в вашей базе данных ничего не будет зафиксировано.

Если ваши операции с базой данных не зависят друг от друга и могут выполняться параллельно, doob ie также позволит вам это сделать. Фиксация транзакций через doob ie, как последовательно, так и параллельно, довольно хорошо объясняется в документации .

...