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

В мире java, зачастую (но не всегда), каждая операция выполняется в своём потоке. И тут всё получается довольно просто, можно воспользоваться ThreadLocal объектом и получать его в любом момент выполнения операции:

class Context {
  public static final ThreadLocal<Context> global = new ThreadLocal<Context>;
}
 
//где-то в месте вызова операции
Context context = new Context(...);
Context.global.set(context);
try {
  someService.someMethod();
} finally {
  Context.global.set(null);
}

В scala же, зачастую, всё не так просто, и по ходу операции поток может смениться неоднократно, например в очень асинхронных приложениях. И способ с ThreadLocal уже не подходит (как и в случае с переключением потоков в java, конечно же).

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

def foo(bar: Bar)(implicit context: Context)

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

Допустим, наш контекст выглядит вот так:

//data - склад для всякой информации касательно операции
class Context(val operationId: String, val data: TrieMap[String, String] = TrieMap.empty)

Создадим трейты, которыми будем помечать контекстно зависимые объекты:

trait ContextualObject {
  protected def context: Option[Context]
}
 
//объект, способный менять свой контекст
trait ChangeableContextualObject[T <: ContextualObject] extends ContextualObject {
  def withContext(ctx: Option[Context]): T
}
 
//объект с пустым контекстом
trait EmptyContext {
  _: ContextualObject =>
 
  override protected val context: Option[Context] = None
}

Теперь объявим наши сервисы и реализации:

//Говорим, что наш сервис может изменять контекст
trait ServiceA extends ChangeableContextualObject[ServiceA] {
  def someSimpleOperation: Int
 
  def someLongOperation(implicit executionContext: ExecutionContext): Future[Int]
}
 
trait ServiceAImpl extends ServiceA {
 
  override def someSimpleOperation: Int = 1
 
  override def someLongOperation(implicit executionContext: ExecutionContext): Future[Int] = {
    Future(someSimpleOperation)
      .map { res =>
        //запишем какие-нибудь данные в контекст выполнения, если он присутствует
        context.foreach(_.data.put("ServiceA.step1", res.toString))
        res * Random.nextInt(10)
      }
      .map { res =>
        context.foreach(_.data.put("ServiceA.step2", res.toString))
        res - Random.nextInt(5)
      }
      .andThen {
        case Success(res) => context.foreach(_.data.put("ServiceA.step3", res.toString))
      }
  }
 
  //создаём сервис с нужным нам контекстом
  override def withContext(ctx: Option[Context]): ServiceA = new ServiceAImpl {
    ctx.foreach(_.data.put("ServiceA.withContext", "true"))
    override protected def context: Option[Context] = ctx
  }
}
 
object ServiceAImpl {
  def apply(): ServiceAImpl = new ServiceAImpl with EmptyContext
}

И второй сервис, который будет использовать первый:

trait ServiceB extends ChangeableContextualObject[ServiceB] {
  def someOperationWithoutServiceA: Int
 
  def someOperationWithServiceA(implicit executionContext: ExecutionContext): Future[Boolean]
}
 
/**
  * При просмотре предыдущего и текущего сервиса мог возникнуть вопрос: 
  * почему это не класс и почему сервис А указан как абстрактный метод?
  * частично ответом является примешивание EmptyContext при создании сервиса,
  * но основная причина заключена в функции withContext.
  * Также, как бонус, в этом случае можно использовать cake pattern при создании объекта
  */
trait ServiceBImpl extends ServiceB {
  self =>
  protected def serviceA: ServiceA
 
  override def someOperationWithoutServiceA: Int = 1
 
  override def someOperationWithServiceA(implicit executionContext: ExecutionContext): Future[Boolean] = {
    serviceA.someLongOperation.map {
      case res if res % 2 == 0 =>
        context.foreach(_.data.put("ServiceB.res", "even"))
        true
 
      case res =>
        context.foreach(_.data.put("ServiceB.res", "odd"))
        false
    }
  }
 
  override def withContext(ctx: Option[Context]): ServiceB = new ServiceBImpl {
    ctx.foreach(_.data.put("ServiceB.withContext", "true"))
    override protected val context: Option[Context] = ctx
    // собственно, тот факт, что мы объявили сервис А как функцию
    // позволяет нам переопределить ее как lazy val,
    // и этот сервис будем инициализирован с новым контекстом, только если это будет нужно.
    // Это я и назвал распространением контекста
    override protected lazy val serviceA: ServiceA = self.serviceA.withContext(ctx)
  }
}
 
object ServiceBImpl {
  // Есть небольшой недостаток - нужно либо называть аргументы именами отличными от тех,
  // что используются в классе, либо помещать их в отдельную переменную внутри функции.
  // Но есть еще вариант объявлять так:
  // class Builder(val serviceA: ServiceA) extends ServiceBImpl with EmptyContext
  // И в месте вызова:
  // new ServiceBImpl.Builder(serviceA)
  // Имя, возможно, не самое удачное, но идея должна быть понятна.
  def apply(a: ServiceA): ServiceBImpl = new ServiceBImpl with EmptyContext {
    //  а в этом месте его можно объявить как val
    override protected val serviceA: ServiceA = a
  }
}

В итоге, в месте вызова мы получим следующий код:

val context = new Context("opId")
val serviceBWithContext = serviceB.withContext(Some(context))
serviceBWithContext.someOperationWithoutServiceA
context.data.get("ServiceB.withContext") // Some("true")
context.data.get("ServiceA.withContext") // None
 
serviceBWithContext.someOperationWithServiceA.andThen {
  case _ => 
    context.data.get("ServiceA.withContext") // Some("true")
    context.data.get("ServiceA.step1") // Some("1")
}

Всё довольно просто — таким образом, по ходу операции будет один и тот же контекст. Но нужно для этого всего найти какое-то реальное применение. Например, мы в ходе операции записывали важную информацию, и теперь хотим эту информацию залогировать. Самым простым вариантом было создавать логгер для каждого контекста, и при записи в лог к сообщению приписывать эту информацию в нём. Но появляется проблема логирования, которое происходит вне вашего кода (например, в сторонней библиотеке).

Для того, чтобы контекст можно было использовать вне нашего кода, сделаем ThreadLocal с нашим контекстом:

object Context {
  val global: ThreadLocal[Option[Context]] = ThreadLocal.withInitial[Option[Context]](() => None)
 
  //Запустить операцию в контексте
  def runWith[T](context: Context)(operation: => T): T = {
    runWith(Some(context))(operation)
  }
 
  //Запустить операцию в контексте
  def runWith[T](context: Option[Context])(operation: => T): T = {
    val old = global.get()
    global.set(context)
    // после завершения вернем старое значение на всякий случай
    try operation finally global.set(old) 
  }
}

Например, если вы используете библиотеку logback-classic для логирования, то вы можете написать свой Layout для логирования этих параметров.

Возможная реализация
class OperationContextLayout extends LayoutBase[ILoggingEvent] {
  private val separator: String = System.getProperty("line.separator")
 
  override def doLayout(event: ILoggingEvent): String = {
    val sb = new StringBuilder(256)
    sb.append(event.getFormattedMessage)
      .append(separator)
 
    appendContextParams(sb)
    appendStack(event, sb)
    sb.toString()
  }
 
  private def appendContextParams(sb: StringBuilder): Unit = {
    Context.global.get().foreach { ctx =>
      sb.append("operationId=")
        .append(ctx.operationId)
 
      ctx.data.readOnlySnapshot().foreach {
        case (key, value) =>
          sb.append(" ").append(key).append("=").append(value)
      }
 
      sb.append(separator)
    }
  }
 
  private def appendStack(event: ILoggingEvent, sb: StringBuilder): Unit = {
    if (event.getThrowableProxy != null) {
      val converter = new ThrowableProxyConverter
      converter.setOptionList(List("full").asJava)
      converter.start()
 
      sb.append()
    }
  }
}


Возможный конфиг
<configuration>
 
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="operation.context.logging.OperationContextLayout" />
        </encoder>
    </appender>
 
    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>


И попробуем что-нибудь залогировать:

  def runWithoutA(): Unit = {
    val context = Some(createContext())
    val res = serviceB.withContext(context).someOperationWithoutServiceA
    Context.runWith(context) {
      // Result of someOperationWithoutServiceA: '1'
      // operationId=GPapC6JKmY ServiceB.withContext=true
      logger.info(s"Result of someOperationWithoutServiceA: '$res'")
    }
  }

  def runWithA(): Future[_] = {
    val context = Some(createContext())
    serviceB.withContext(context).someOperationWithServiceA.andThen {
      case _ =>
        Context.runWith(context) {
          // someOperationWithServiceA completed
          // operationId=XU1SGXPq1N ServiceB.res=even ServiceA.withContext=true ServiceB.withContext=true ServiceA.step1=1 ServiceA.step2=7 ServiceA.step3=4
          logger.info("someOperationWithServiceA completed")
        }
    }
  }

И остался вопрос: как же быть с внешним кодом, который запускается в ExecutionContext? Но нам же никто не мешает написать враппер для него:

Возможная реализация враппера
class ContextualExecutionContext(context: Option[Context], executor: ExecutionContext) extends ExecutionContext {
 
  override def execute(runnable: Runnable): Unit = executor.execute(() => {
    Context.runWith(context)(runnable.run())
  })
 
  override def reportFailure(cause: Throwable): Unit = {
    Context.runWith(context)(executor.reportFailure(cause))
  }
 
}
 
object ContextualExecutionContext {
  implicit class ContextualExecutionContextOps(val executor: ExecutionContext) extends AnyVal {
    def withContext(context: Option[Context]): ContextualExecutionContext = new ContextualExecutionContext(context, executor)
  }
}


Возможная реализация внешней системы
class SomeExternalObject {
  val logger: Logger = LoggerFactory.getLogger(classOf[SomeExternalObject])
 
  def externalCall(implicit executionContext: ExecutionContext): Future[Int] = {
    Future(1).andThen {
      case Success(res) => logger.debug(s"external res $res")
    }
  }
}

Попробуем сделать вызов в нашем ExecutionContext:


  def runExternal(): Future[_] = {
    val context = Some(createContext())
    implicit val executor = global.withContext(context)
    // external res 1
    // operationId=8Hf277SV7B
    someExternalObject.externalCall
  }

Вот и вся идея. На самом деле, использование контекста не ограничивается только логированием. Можно хранить в этом контексте всё, что угодно. Например, слепок каких-то состояний, если нужно, чтобы все сервисы во время операции работали с одинаковыми данными. И так далее, и так далее.

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

P.S. Исходный код проекта, используемый в статье github.com/eld0727/scala-operation-context.
P.P.S. Я уверен, что данный подход может быть применен и к других языкам, позволяющим создавать анонимные классы, и это всего лишь возможная реализация на scala.
Поделиться с друзьями
-->

Комментарии (30)


  1. NaHCO3
    29.05.2017 14:34

    Монада Reader, комонада CoReader как раз для таких вещей и были сделаны. А ещё ваша идея мне напомнила имплиситные функции, о которых Одерски распространялся на недавнем Scala Days: https://www.youtube.com/watch?v=Oij5V7LQJsA&list=PLLMLOC3WM2r5Ei2mnSHCD-ZD04AXovttL&index=1

    Вы пробовали проанализировать уже существующие инструменты и понять, почему они не подходят, и какие изменения нужны?


    1. eld0727
      29.05.2017 15:08

      Про имплиситы:

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

      или я вас не понял

      Монада Reader, комонада CoReader как раз для таких вещей и были сделаны

      Как будут по вашему выглядеть сервисы, если всё вокруг обвешать ридерами?

      Этот способ позволяет минимизировать различие между кодом с и без контекста.

      Честно мне не попадалось на глаза решения подобной проблемы


      1. NaHCO3
        29.05.2017 15:42

        > Про имплиситы я вас не понял

        Есть такая мысль, вместо имплиситов-аргументов, использовать имплиситы типы. Вместо def doLogic(implicit ctx: Context): Int использовать type MyContext[Int], который является функцией, которая принимает в себя имплиситный аргумент и возвращает соответствующий ему Function0[Int] (каррирование). Это Одерски предлагал в указанном мною видео.

        Ещё про имплиситные контексты есть такой забавный трюк: https://github.com/scala-native/scala-native/blob/master/util/src/main/scala/scala/scalanative/util/Scope.scala

        > Как будут по вашему выглядеть сервисы, если всё вокруг обвешать ридерами?

        Это только поначалу неудобно. Когда вы втягиваетесь в функциональное программирование, становится трудно остановиться. Сначала монады, потом трансформеры, потом Free, потом eff.


        1. eld0727
          29.05.2017 16:05

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

          Это только поначалу неудобно. Когда вы втягиваетесь в функциональное программирование, становится трудно остановиться. Сначала монады, потом трансформеры, потом Free, потом eff.


          Звучит очень опасно :) Это как бы использовать вилку, чтобы есть суп, только по началу не удобно, вот такая вот ассоциация


          1. NaHCO3
            29.05.2017 16:30

            Это больше похоже на то, как есть рис вилкой. Поначалу непонятно, зачем вообще нужны столовые приборы, когда и руками получается отлично. Зачем ещё лишние сущности плодить? Потом человек осваивает вилку, и вроде неплохо получается, есть свои удобства.

            Дальше следуют остальные столовые приборы и способы их применения. И со временем человек просто перестаёт их замечать, они откладываются в подкорку. Если, конечно, его не посетит желание устроить холивар — чем правильнее есть рис, палочками или вилкой?

            А на самом деле правильнее есть рис, чем его не есть. А чем именно — палочками, вилкой или руками, уже не так принципиально.


            1. eld0727
              29.05.2017 16:44

              Вот с последним выводом, нельзя не согласится


  1. notxcain
    29.05.2017 15:24
    +1

    Scala — прекрасный язык! Зачем вы так с ним? Вот решение с чистыми функциями, все что нужно — это понимать HKT и монады.


    1. NaHCO3
      29.05.2017 15:58
      +1

      Рисковый вы парень, делать import сats._ Мне вот всегда подобное страшно, каких ещё имплиситных трансформацией принесёт с собой библиотека. Как она будет реагировать с другими обобщёнными библиотеками? Что нужно заимпортировать, чтобы заставить работать примеры с вебсайта? Допустим, примеры работают. Я слегка изменил код, и он снова перестал работать — что нужно импортировать теперь?


      1. eld0727
        29.05.2017 16:03

        жиза xD


      1. notxcain
        29.05.2017 16:36
        +2

        Если это самое плохое в примере — ок :) У Cats все хорошо с имплиситам, они предсказуемы. Во всяком случае я пока ни разу не обжигался.


        1. NaHCO3
          30.05.2017 11:00

          > Если это самое плохое в примере — ок :)

          Если честно, я не вчитывался. Поверил на слово, что у вас там и в самом деле HKT и монады. Функциональное программирование обычно обладает свойством, что одну и ту же задачу в нём нельзя решить разными способами. Унификация функционального программирования — это ответ инкапсуляции объектно-ориентированного. Тебе не надо тратить кучу времени на изучение библиотеки, если она инкапсулирована и публикует только несколько полезных для конечных пользователей точек входа. Функциональную библиотеку тоже легко изучить — потому что сделана она может быть только одним способом.

          Об этом можно судить по столь нежно любимым всеми функциональщиками коммутативным диаграммам. Там есть пунктирный подвид стрелочек — «существует единственный морфизм». На этом единстве и построена вся теория. Произведение и копроизведение типов так и определяется — через единственность (с точностью до изоморфизмов). Тут можно заметить, что технологии скалы не позволяют никак задать единственность и какое-либо приведение к каноническому виду. Меня, например, всегда огорчает что вывод типов не умеет обрабатывать sealed классы, доказывая что экземпляр класса может быть только одним из его наследников, указанных по списку, и никакого другого не существует. Понятное дело, имплиситы — очень мощный инструмент, и на них можно что-нибудь закостылить на базе HType, но это всё противоестественно.

          И вообще, скала всё же скверно приспособлена к функциональному программированию. Буйство функциональных абстракций — это неожиданное применение инструмента, создававшегося для совсем других целей. Скала как язык и в ещё большей степени её тулинг плохо приспособлены для работы с таким количеством имплиситов. Когда я разбираюсь с ООП библиотекой здорового человека — я могу посмотреть в скаладок для конкретного класса, или создать его экземпляр в REPL и проверить, как он работает. Это чем-то напоминает работу в командной строке, все сложности — внутри, а снаружи — достаточно простые команды.

          Другое дело создания сумрачного гения в функциональном стиле. REPL бесполезен, скаладок бесполезен — остаётся только читать исходники. Не идёт ни какой речи о сокрытии сложностей — все они закладываются прямо в структуру данных, которыми ты должен обмениваться с библиотекой. Одно дело в библиотеку передать Int, другое дело — монадный трансформер от Int. Ты уже должен понимать как работает внутри библиотека. А из-за имплиситов ты даже представления не имеешь, где искать нужные тебе библиотечные функции. И чёрт с ними, с исходниками, можно и их почитать, но хотелось бы понять, какой именно файл нужен. Имплиситы хорошо работают в уже существующем проекте, когда IDE любезно раскрывает для пользователя всю незаметную глаз машинерию. Но и оно бессильно, когда ты только хочешь задействовать имплиситную библиотеку, или вовсе на бумажке схему бизнес-логики рисуешь.

          Поэтому я стараюсь не связываться с имплиситщиной, только если без неё совсем трудно обходиться. Особенно со сторонними библиотеками. Там лежит куча функций, которые мне не нужны, но я вынужден прочитать все исходники, чтобы найти нужную. А ещё спустя несколько лет и десяток версий там что-нибудь изменится, например именование, и мне придётся переписывать свой код и импорты. А я не хочу следить за редкоиспользуемой библиотекой. И если я всё равно читаю чужие исходники, то что мне мешает написать свои собственные? Которые делают примерно тоже самое, но без развесистой клюквы, сугубо в необходимых мне объёмах. Тем более, что все эти монады — вещи чрезвычайно тривиальные и однотипные.

          Это как давний спор про номинальные и структурные типы. Что лучше использовать — (Double, Double, Double) или final case class Gas(pressure: Double, temperature: Double, volume: Double). Сторонники ФП предпочитают первый тип, меня же не напрягает написать полностью второй вариант — он оказывается более понятным при последующем чтении.

          При этом я вполне осведомлён о монадах и прочих зверьках ФП. Эти знания позволяют не изобретать велосипед, не путаться в своих мыслях, понимать задумки других программистов, вместо долгого вникания в код сразу понять о чём речь. В точности как набор объектно-ориентированных шаблонов проектирования банды четырёх. Тут главное не реализация, не готовые библиотеки, а понимание и умение находить в реальных проектах эти шаблоны, выстраивать эквивалентности между конкретным сложным кодом и набором простых идей.

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


    1. eld0727
      29.05.2017 16:08

      Весь этот пример ломается об необходимость делать на фучах recover. Которого нет в Monad и усё, нет ни контекста, нифига.

      Если не прав, попровьте, плиз


      1. notxcain
        29.05.2017 16:34
        +1

        Для таких случаев есть `MonadError`


  1. fr33zy
    29.05.2017 15:43

    А такой вариант с MDC смотрели? Благодаря такому подходу, в логах даже у других либ выводится контекст,


    типа

    [2017-05-29 15:00:000] [debug] [user-id] [trace-id] our.class.a GET /foo/bar/baz
    [2017-05-29 15:00:005] [trace] [user-id] [trace-id] our.class.b Getting things
    [2017-05-29 15:00:010] [trace] [user-id] [trace-id] io.getquill.JdbcContext SELECT * FROM Blablabla WHERE id = 'baz'


    1. eld0727
      29.05.2017 15:44

      честно, такое у нас тоже есть, но меня напрягает что на каждом таске происходит одно чтение из тред локали и 2 записи. И эта же идея используется врапере для контекста, кстати


  1. wert_lex
    29.05.2017 16:18
    +2

    Разработка на Scala как она есть. Вообще нам в половину сервисов надо передавать неявно контекст, а в половину не надо. Поэтому мы не будем использовать implicit (прим. пер. — неявно) и дефолтный аргумент для него, а запилим свой микрофреймворк на 5kloc на шаблонах и с макросами, подвязанный через недокументированное api sbt и без документации, но зато с веб-интерфейсом на локалхосте, и вот оно :)


  1. Mishkun
    29.05.2017 19:25

    Как я понял, вы пытались изобрести комонады, но в итоге вас что-то столкнуло с чистого пути и вы выкрутились применяя ООП


    1. eld0727
      29.05.2017 19:30

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


  1. PHmaster
    30.05.2017 01:34

    Что-то я упустил пасс руками в том месте, где ThreadLocal помог передать контекст в стороннюю библиотеку. Ну то есть понятно, когда она синхронная — это сработает. А если она асинхронная? Когда-то мучался с этим, пытался прикрутить MDC через кастомный диспетчер, как в примере выше к асинхронному сетевому приложению, но так вопрос и не решил: контекст терялся на границе запрос-ответ. То есть, запрос отправляется по сети, MDC сохраняется в ThreadLocal, а потом в этот поток приходит ответ на совершенно другой запрос, и MDC из ThreadLocal привязывается не к тому ответу. Учитывая, что там обрабатывалось до 1к параллельных запросов на пуле из 12 потоков, путаница получалась та еще. Может, существует какое-то рабочее решение этой проблемы?


    1. eld0727
      30.05.2017 10:59

      Есть подозрение, что вы как то неправильно его используете. У нас крутится несколько приложений на подходе похожем на MDC. Для того, чтобы это работало, нужно:

      1. чтобы все операции исполнялись в ExecutorContext'ах, которые реализуют логику по изменению ThreadLocal
      2. по дороге не было ничего, что может потерять контекст. Например, акторы, но есть решение и для них


      Количество тасков в контекстах не играет никакой роли. Если опишите поподробнее ситуацию, я скорее всего подскажу как её решить


      1. PHmaster
        30.05.2017 12:50

        Ну описал насколько мог подробно, других деталей сейчас не вспомню. И да, приложение было как раз на акторах, решение — на MDCPropagatingDispatcher. Контекст терялся именно на сетевых запросах-ответах, в остальных местах вроде как все работало.


        Добавлено: Я просто не уверен, что понимаю, как ThreadLocal может спасти ситуацию, если контекст нужно передать между тредами, а не оставить в текущем.


        1. eld0727
          30.05.2017 13:45

          ситуацию спасает scala.concurrent.ExecutionContext#prepare собственно его дергает Future перед запуском таска. То есть на вызове этого метода создается новый инстанс контекста, в который можно записать что либо. Или который как раз будет при запуске затаска заполнять ThreadLocal нужным значением


          1. PHmaster
            03.06.2017 01:30

            Спасибо за подсказку насчет prepare(), будет случай — попробую применить. На данный момент у меня проблема с Netty, к которому я попытался прикрутить злополучный MDC. И опять ничего не получается. По запросу Netty MDC гуглится вот это решение, но оно мало того, что немного криво написано — так еще и совсем не работает. Наверное, автор его для однопоточного режима писал, или для более старой версии Netty, и с условием, что значения MDC выставляются при создании ExecutionContext один раз и больше не меняются (мне нужно, чтобы они подхватывались из текущих актуальных значений в MDC.getCopyOfContextMap() каждый раз при вызове execute(), но это легко поправимо). Но даже с внесенными мной изменениями оно не заработало: я попытался помимо exec() провернуть подобный фокус со всеми вариациями методов submit() и schedule(), а также переопределить метод newChild() который создает дочерние EventLoop (которые являются по сути SingleThreadEventExecutor и реализуют ExecutorService), и у них также переопределить execute(), schedule() и submit() во всех вариациях. Результатат ноль. Это, правда, уже не Scala, а Java, но тоже асинхронная.


            1. PHmaster
              03.06.2017 04:02

              Хмм, нашел свой старый код, который пытался прикрутить к Netty пару лет назад. Там действительно используется ExecutionContext#prepare, и оставлен коммент, что все хорошо, пока не нужно взаимодействовать с Java-библиотеками. Так как в Java всякие Executor/ExecutorService не имеют механизма, аналогично скаловскому ExecutionContext#prepare. Для этого у меня там еще несколько вспомогательных классов: обертки для Runnable/Callable, и обертки для Executor/ExecutorService, которые заворачивают Runnable/Callable в их обертки, чтобы вместе с ними передать MDC.getCopyOfContextMap(). А дальше идет коммент, что с Netty все это все равно не работает :) Так как у них там NioEventLoopGroup, который создает дочерние EventLoop, причем реализацию, которая объявлена final и поэтому ее нельзя унаследовать и переопределить нужные методы. И фасад для нее нельзя сделать, так как она в нескольких методах передает this вовне, и фасад отваливается. В общем, внутри scala все красиво, а вот на стыке scala-java подобные нестыковки случаются частенько, к сожалению :(


              1. eld0727
                03.06.2017 10:56

                Ну вообще говоря эту линию между scala и java миром можно сгладить восстановив контекст при возвращении в scala. Примерный псевдокод:

                val context = MDC.getCopyOfContextMap()
                runMethodThatWillInteractWithJavaAsync().andThen {
                  case _ => MDC.setContext(context) // я точно не знаю какой тут должен быть метод
                }.map { result => ... //Тут у вас уже опять родной контекст и далее по пути следования
                }
                


                Да вы можете потерять часть логов внутри java библиотеки, но это, я считаю, совсем мелочи


                1. PHmaster
                  03.06.2017 14:21

                  В моем случае как раз совсем не мелочи, так как netty пишет свои логи без MDC, и при куче параллельных запросов все превращается в кровавую мешанину. А мне как раз эти логи и нужно привязывать к контексту, чтобы потом разгребать. Я зарепортил этот "баг" в netty, посмотрю, что там на это скажут.


  1. fogone
    30.05.2017 10:23

    Но нужно для этого всего найти какое-то реальное применение.
    Сначала напедалить, а потом думать зачем — всегда так делаю.
    Можно хранить в этом контексте всё, что угодно.
    Это очень полезный горшок.

    Это я к тому, что не совсем понятно, какую именно проблему мы решаем. Если всё это только ради того, чтобы вынести логирование из сервисного метода, то сложно себе представить, что оно того стоит. А если передавать какую-то значимую информацию в этом контексте, то это резко увеличивает неявную связанность между сервисами и лично я бы старался такого избегать до того момента, пока другого варианта просто нет. Можно себе представить, что мы хотим например закешировать полученные из бд сущности, чтобы увеличить производительность, но такой ценой я бы делал это в последнюю очередь.


    1. eld0727
      30.05.2017 10:54

      Логирование — это самый простой и примитивный пример.
      Если опять же говорить про веб бекенд, то там можно хранить сессию и пользователя например, но это наверное спорное применение.
      Но вот я в статье упомянул, что можно передавать слепок состояния. То есть у тебя есть какие то данные, которые переодически обновляются (например кеш конфигов приложения или справочники). И вот перед тобой стоит задача — гарантировать, что при выполнении одного запроса эти состояния будут одинаковыми во всех сервисах. И вот подобный контекст уже отлично ложится на эту ситуацию. Мы взяли вначале операции сделали snapshot состояния, и сделали всё что нужно с используя его.

      Вобщем проблема не разу не подгонялась под решение, решение радилось из-за проблемы


      1. fogone
        30.05.2017 15:50

        не совсем понимаю, если это данные которые параметризируют выполнение метода, то почему тогда его не передать в качестве параметра? это по крайней мере не будет создавать иллюзию и будет добавлять чистоты методу в функциональном смысле. И как вызывающий поймет, что ему необходимо передать какой-то контекст? Для этого обычно используется явно объявление параметров метода, которые гарантирют заодно, что его без этих параметров не вызовешь на этапе компиляции еще. А так получается, что вроде всех перехитрили: и компилятор и вызывающего этот метод заодно.


        1. eld0727
          31.05.2017 12:06

          Не очень понимаю как я перехитрил компилятор и метод. По поводу использования в сигнатуре метода я писал.

          Первое, что может прийти в голову, это передавать контекст через имплиситный аргумент функции.
          def foo(bar: Bar)(implicit context: Context)
          Но это будет захламлять протокол сервисов.


          И как вызывающий поймет, что ему необходимо передать какой-то контекст?


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

          Для этого обычно используется явно объявление параметров метода


          да, если этих метод полторы штуки, то ок. Но если вам нужно чтобы весь код нуждается в контесте, тоооо будет жутко мазолить глаза. Опять же это решится с ImplicitFunction как и упомяналось выше