N|Solid


В нашем отделе бигдаты часть данных хранится в Aerospike. Потребителей довольно много, среди них два приложения, написанных на Scala, взаимодействие с базой в которых будет расширено в связи с постоянно растущими требованиями бизнеса. Единственным приличным драйвером для нас был джавовый клиент, упомянутый на сайте самой базы данных aerospike.com (http://www.aerospike.com/docs/client/java). Конвертация скаловых типов данных (а особенно иерархических) в соответствующие аэроспайковские типы приводит к большому количеству бойлерплейта. Чтобы этого избежать, необходим более удобный, а заодно и типобезопасный интерфейс.


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



Немного про Aerospike


Aerospike — это распределённая schema-less key-value база данных, работающая по принципу хэш-таблицы. Она активно используется в нашем банке для построения распределённых кэшей и для задач, требующих низкого времени отклика. База легко устанавливается и без проблем администрируется, что упрощает её внедрение и поддержку.


О модели хранения: параметры namespace и setName связаны с ключами записей, а сами данные хранят в так называемых бинах. Значения могут быть различных типов: Integers, Strings, Bytes, Doubles, Lists, Maps, Sorted Maps, GeoJSON. Интересно, что тип бина не является фиксированным и, записав, скажем, Integer, можно затем перезаписать его на любой другой. Драйверы, написанные для этой базы, обладают изрядным количеством кода для сериализации значений внешней модели во внутреннюю.


Про создание DSL


Рассмотрим на простых примерах процесс проектирования нашего DSL, почему мы решили использовать макросы, и что из этого всего получилось.


В условиях ограниченного времени (взаимодействие с этой базой только малая часть проекта) сложно написать целиком клиент с реализацией протокола. К тому же это потребовало бы больше усилий в поддержке. Поэтому мы остановились на создании обёртки для уже существующего клиента. Рассмотрим на примерах.


В качестве базиса использован Aerospike Java Client версии 3.3.1 (его можно найти на сайте www.aerospike.com, исходники есть на Гитхабе), немалая часть методов в котором оперирует с ключами и бинами из пакета com.aerospike.client. Java Client поддерживает работу с базой как в синхронном, так и в асинхронном режиме. Мы используем асинхронный com.aerospike.client.async.AsyncClient. Самый простой способ его создать:


val client = new AsyncClient(new AsyncClientPolicy, hosts.map(new Host(_, port)): _*)

где hosts — это List[String], содержащий хосты вашей базы, а port — порт типа Int (по дефолту 3000).


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


scala> new AsyncClient(new AsyncClientPolicy, List().map(new Host(_, port)): _*)
com.aerospike.client.AerospikeException$Connection: Error Code 11: Failed to connect to host(s):

Таблица соответствий типов в DSL, Java CLient и базе данных


| Scala         | Java Client   | Aerospike     |
|-------------- |-------------- |-----------    |
| Int           | IntegerValue  | Integer       |
| Long          | LongValue     | Integer       |
| String        | StringValue   | String        |
| Boolean       | BooleanValue  | Integer       |
| Float         | FloatValue    | Double        |
| Double        | DoubleValue   | Double        |
| Seq           | ListValue     | List          |
| Map           | MapValue      | Map           |
| Char          | StringValue   | String        |
| Short         | IntegerValue  | Integer       |
| Byte          | IntegerValue  | Integer       |
| HList         | MapValue      | Map           |
| case class T  | MapValue      | Map           |

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


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


В методах интерфейса нашей DSL для любых действий с базой мы будем передавать только конкретные значения ключей и бинов, а все преобразования за нас сделают макросы. Основная идея была в том, чтобы избавиться от бойлерплейта и уберечь пользователя от досконального изучения внутренней структуры данных самого Aerospike. Мы предварительно описали наиболее оптимальный вариант хранения, опираясь на тип переданного для записи значения.


Рассмотрим на примере одной из самых распространенных операций с Aerospike — добавления записи с последующим ее чтением по ключу. Будем использовать метод Put. Для начала нам нужны функции преобразования значений определенных типов во внутренние модели драйвера: ключей в com.aerospike.client.Key, а бинов в com.aerospike.client.Bin.
Пусть ключ будет String, а записывать в различных сервисах будем бины типов String, Int, Boolean.


Напишем функцию преобразования ключа:


import com.aerospike.client.Key
def createStringKey(namespace: String, setName: String, value: String): Key =
   new Key(namespace, setName, new StringValue(value))

и бинов соответственно:


import com.aerospike.client.Value.{IntegerValue, StringValue, BooleanValue}

def createStringBin(name: String, value: String): Bin = new Bin(name, new StringValue(value))
def createIntBin(name: String, value: Int): Bin = new Bin(name, new IntegerValue(value))
def createBooleanBin(name: String, value: Boolean): Bin = new Bin(name, new BooleanValue(value))

Сигнатура нужного нам метода в библиотеке на java (вариантов несколько, мы берем с наименьшим количеством параметров):


public void put(WritePolicy policy, Key key, Bin... bins) throws AerospikeException;

Значит, вызовы с использованием этой библиотеки будут выглядеть так:


import com.aerospike.client.policy.WritePolicy

client.put(new WritePolicy, createStringKey("namespace", "setName", "keyValue1"),
   Seq(createStringBin("binName1", "binValue1"), createStringBin("binName2", "binValue2")): _*)
client.put(new WritePolicy, createStringKey("namespace", "setName", "keyValue2"),
   Seq(createIntBin("binName1", 2), createIntBin("binName2", 4)): _*)
client.put(new WritePolicy, createStringKey("namespace", "setName", "keyValue3"),
   Seq(createBooleanBin("binName1", true), createBooleanBin("binName2", false)): _*)

Не слишком симпатично, правда? Попробуем упростить:


 def createKey[T](ns: String, sn: String, value: T): Key = {
   val key = value match {
     case s: String => new StringValue(s)
     case i: Int => new IntegerValue(i)
     case b: Boolean => new BooleanValue(b)
     case _ => throw new Exception("Not implemented")
   }
   new Key(ns, sn, key)
 }

 def createBin[T](name: String, value: T): Bin = {
   value match {
     case s: String => new Bin(name, new StringValue(s))
     case i: Int => new Bin(name, new IntegerValue(i))
     case b: Boolean => new Bin(name, new BooleanValue(b))
     case _ => throw new Exception("Not implemented")
   }
 }

 def putValues[K, B](client: AsyncClient, namespace: String, setName: String,
                     keyValue: K, bins: Seq[(String, B)])(implicit wPolicy: WritePolicy): Unit = {
   client.put(wPolicy, createKey(namespace, setName, keyValue), bins.map(b => createBin(b._1, b._2)): _*)
 }

Теперь надо избавиться от функций createKey и createBin, добавим магии имплиситов.


Нам понадобятся служебные объекты, которые будут на основе типов входных данных генерировать соответствующие модели используемого драйвера:


KeyWrapper: [K => Key]
BinWrapper: [B => Bin]

Теперь можно собрать всю логику в один метод:


case class SingleBin[B](name: String, value: B)

def putValues[K, B](client: AsyncClient, key: K, value: SingleBin[B])(implicit kC: KeyWrapper[K],
 bC: BinWrapper[B], wPolicy: WritePolicy): Unit = client.put(wPolicy, kC(key), bC(value))

где WritePolicy — объект контейнер, содержащий различные параметры записи. Мы будем пользоваться дефолтным, создавая его так new WritePolicy.


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


Простейший вариант — описать создание того или иного типа конвертера при помощи квазиквот. Начнем с ключей:


 trait KeyWrapper[KT] {

   val namespace: String = ""
   val setName: String = ""

   def apply(k: KT): Key

   def toValue(v: KT): Value = v match {
     case b: Int => new IntegerValue(b)
     case b: String => new StringValue(b)
     case b: Boolean => new BooleanValue(b)
     case _ => throw new Exception("not implemented")
   }
 }

 object KeyWrapper {

   implicit def materialize[T](implicit dbc: DBCredentials): KeyWrapper[T] = macro impl[T]

   def impl[T: c.WeakTypeTag](c: Context)(dbc: c.Expr[DBCredentials]): c.Expr[KeyWrapper[T]] = {
     import c.universe._
     val tpe = weakTypeOf[T]

     val ns = reify(dbc.splice.namespace)
     val sn = reify(dbc.splice.setname)

     val imports =
       q"""
         import com.aerospike.client.{Key, Value}
         import collection.JavaConversions._
         import com.aerospike.client.Value._
         import scala.collection.immutable.Seq
         import ru.tinkoff.aerospikescala.domain.ByteSegment
         import scala.util.{Failure, Success, Try}
        """

     c.Expr[KeyWrapper[T]] {
       q"""
       $imports
       new KeyWrapper[$tpe] {
         override val namespace = $ns
         override val setName = $sn
         def apply(k: $tpe): Key = new Key(namespace, setName, toValue(k))
       }
      """
     }
   }
 }

где DBCredentials содержит namespace и setName.


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


N|Solid


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


public Record get(Policy policy, Key key) throws AerospikeException;

где возвращаемое значение:


public Record(
     Map<String,Object> bins,
     int generation,
     int expiration
  )

а необходимые нам данные лежат в Map<String,Object> bins. Тут возникает проблема (см. таблицу соответствий). Так как наша цель — генерировать конвертеры на этапе компиляции и обеспечить на выходе значение типа, идентичного записанному ранее, нам надо предсказать, как именно описать функцию, достающую нужное нам вэлью из базы. Помимо прочего типы, которые мы получаем в bins из пакета java.util — значит, нам пригодятся конвертеры из соответствующих пакетов scala.collection.
Теперь напишем конвертер для бинов:


trait BinWrapper[BT] {

 import com.aerospike.client.Value._
 import com.aerospike.client.{Bin, Record, Value}
 import scala.collection.JavaConversions._
 import scala.collection.immutable.Map
 import scala.reflect.runtime.universe._

 type Singleton = SingleBin[BT]
 type Out = (Map[String, Option[BT]], Int, Int)

 def apply(one: Singleton): Bin = {
   if (one.name.length > 14) throw new IllegalArgumentException("Current limit for bean name is 14 characters")
   else new Bin(one.name, toValue(one.value))
 }

 def toValue(v: BT): Value = v match {
   case b: Int => new IntegerValue(b)
   case b: String => new StringValue(b)
   case b: Boolean => new BooleanValue(b)
   case _ => throw new Exception("not implemented")
 }

 def apply(r: Record): Out = {
   val outValue: Map[String, Option[BT]] = {
     val jMap = r.bins.view collect {
       case (name, bt: Any) => name -> fetch(bt)
     }
     jMap.toMap
   }
   if (outValue.values.isEmpty && r.bins.nonEmpty) throw new ClassCastException(
     s"Failed to cast ${weakTypeOf[BT]}. Please, implement fetch function in BinWrapper")
   else (outValue, r.generation, r.expiration)
 }

 def fetch(any: Any): Option[BT]
}

Метод apply принимает в качестве параметра Record — тут обобщить можно всё до момента разбора непосредственно типа значения. Реализацию этого метода проще написать на макросах:


object BinWrapper {

 implicit def materialize[T]: BinWrapper[T] = macro materializeImpl[T]

 def materializeImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[BinWrapper[T]] = {
   import c.universe._
   val tpe = weakTypeOf[T]
   val singleton = weakTypeOf[SingleBin[T]]
   val out = weakTypeOf[(Map[String, Option[T]], Int, Int)]
   val tpeSt = q"${tpe.toString}"

   val fetchValue = tpe match {
     case t if t =:= weakTypeOf[String] => q"""override def fetch(any: Any): Option[$tpe] = any match {
       case v: String => Option(v)
       case oth => scala.util.Try(oth.toString).toOption
     } """
     case t if t =:= weakTypeOf[Boolean] => q"""override def fetch(any: Any): Option[$tpe] = any match {
       case v: java.lang.Long => Option(v == 1)
       case _ => None
     } """
     case t if t =:= weakTypeOf[Int] => q"""override def fetch(any: Any): Option[$tpe] = any match {
       case v: java.lang.Long => Option(v.toInt)
       case oth => scala.util.Try(oth.toString.toInt).toOption
     } """
     case t if t.toString.contains("HNil") || t.toString.contains("HList") =>
       q"""override def fetch(any: Any): Option[$tpe] = any match {
             case m: java.util.HashMap[Any, Any] =>
             val newList = castHListElements(m.asScala.values.toList, $tpeSt)
             newList.toHList[$tpe]
             case oth => None
           } """
     case _ => q""""""
   }

   val imports =
     q"""
        import java.util.{List => JList, Map => JMap}
        import com.aerospike.client.{Bin, Record, Value}
        import com.aerospike.client.Value.{BlobValue, ListValue, MapValue, ValueArray}
        import scala.collection.JavaConversions._
        import scala.collection.JavaConverters._
        import shapeless.{HList, _}
        import shapeless.HList.hlistOps
        import syntax.std.traversable._
        ....
      """

   c.Expr[BinWrapper[T]] {
     q"""
     $imports

     new BinWrapper[$tpe] {
       override def apply(one: $singleton): Bin = {
          if (one.name.length > 14) throw new IllegalArgumentException("Current limit for bean name is 14 characters")
          else new Bin(one.name, toValue(one.value))
        }
       override def apply(r: Record): $out = {
          val outValue: Map[String, Option[$tpe]] = {
          val jMap = r.bins.view collect {
           case (name, bt: Any) =>
           val res = fetch(bt)
           if (res.isEmpty && r.bins.nonEmpty) throwClassCast($tpeSt) else name -> res
          }
         jMap.toMap
         }

        (outValue, r.generation, r.expiration)
       }
       $fetchValue
     }

   """
   }
 }
}

Макросы сделали за нас всю работу — инстансы всех требуемых конвертеров будут генерироваться самостоятельно, вызовы методов будут содержать только сами значения ключей и бинов.


N|Solid


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


Теперь у нас есть всё, чтобы написать сервис-обертку:


class SpikeImpl(client: IAsyncClient) {

 def putValue[K, B](key: K, value: SingleBin[B])(implicit kC: KeyWrapper[K], bC: BinWrapper[B]): Unit = {
   val wPolicy = new WritePolicy
   client.put(wPolicy, kC(key), bC(value))
 }

  def getByKey[K, B](k: K)(implicit kC: KeyWrapper[K], bC: BinWrapper[B]): Option[B] = {
   val policy = new Policy
   val record = client.get(policy, kC(k))
   bC.apply(record)._1.headOption.flatMap(_._2)
 }
}

Теперь можно проверить работу нашего сервиса:


import shapeless.{HList, _}
import shapeless.HList.hlistOps
import scala.reflect.macros.blackbox._
import scala.language.experimental.macros

object HelloAerospike extends App {

 val client = new AsyncClient(new AsyncClientPolicy, hosts.map(new Host(_, port)): _*)
 val database = new SpikeImpl(client)
 implicit val dbc = DBCredentials("namespace", "setName")

   database.putValue("key", SingleBin("binName", 123 :: "strValue" :: true :: HNil))
   val hlistBin = database.getByKey[String, Int :: String :: Boolean :: HNil]("key")
     .getOrElse(throw new Exception("Failed to get bin value"))
   println("hlistBin value = " + hlistBin)

}

Запускаем и заходим в базу:


Mac-mini-administrator-5:~ MarinaSigaeva$ ssh user@host
user@host's password:
Last login: Wed Nov 23 19:41:56 2016 from 1.1.1.1
[user@host ~]$ aql
Aerospike Query Client
Version 3.9.1.2
Copyright 2012-2016 Aerospike. All rights reserved.
aql> select * from namespace.setName
+------------------------------------------+
| binName                                  |
+------------------------------------------+
| MAP('{"0":123, "1":"strValue", "2":1}')  |
+------------------------------------------+
1 row in set (0.049 secs)
aql>

Данные записаны. Теперь посмотрим, что приложение вывело в консоль:


[info] Compiling 1 Scala source to /Users/Marina/Desktop/forks/playground/target/scala-2.11/classes...
[info] Running HelloAerospike
hlistBin value = 123 :: strValue :: true :: HNil
[success] Total time: 0 s, completed 23.11.2016 20:01:44

Для scala разработчиков решение может быть более понятным интуитивно, чем java библиотека. Код текущего DSL выложен на Гитхабе с подробным описанием how to и кукбуком, который будет дополняться. В свете последних событий (scala 2.12 released) появилась задача для интересных экспериментов со scala-meta. Надеюсь этот опыт будет вам полезен в решении подобных задач.

Поделиться с друзьями
-->

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


  1. vuspenskiy
    16.12.2016 12:21

    Пеши исчо. Если будут вопросы по scala-meta — пиши, мы с Женей теперь почти соседи 8-)


    1. DanyMariaLee
      16.12.2016 12:27

      Спасибо! Обязательно напишу про работу с мета.
      Какое приятное соседство у тебя)


  1. chemistmail
    16.12.2016 21:13

    2 вопроса:
    1) размер базы
    2) коммерческая версия или бесплатная


    1. chemistmail
      16.12.2016 21:21

      Можно в личку, интересуюсь потому что сам использую. Возможен обмен опытом при возникновении незапланированных ситуаций. (Были такие на моей памяти)

      У меня сейчас:
      Один из Namespace — ов
      Master Objects: 2.278 G
      Disk Used: 2.764 TB
      Mem Used: 271 GB
      Версия бесплатная.


      1. DanyMariaLee
        16.12.2016 21:24

        Если будут вопросы пишите, конечно, обмен опытом всегда на пользу!


    1. DanyMariaLee
      16.12.2016 21:22

      1) можем держать 2,5 тб
      2) бесплатная


  1. ImLiar
    17.12.2016 02:26

    А разве нельзя было то же самое на shapeless зарядить с автоматическим выводом тайп-классов?
    Там еще были бы и рекорды из коробки


    1. DanyMariaLee
      17.12.2016 15:39

      Привет! Я постаралась объяснить в гисте https://gist.github.com/DanyMariaLee/2774f10e64e6aaff2e855061aa74a16c
      Если что-то не достаточно ясно, могу развернуть еще или отправить к литературе соответствующей


      1. ImLiar
        17.12.2016 16:04

        Ммм. Немного некорректно дано определение вывода.

        Автоматический вывод тайп-классов — это фактически вывод тайп-класса для некого типа `T` на основе уже имеющейся информации. Например, если у вас описаны тайп-классы для примитивов, можно вывести через LabelledGeneric для кейс-классов. А через копродукты еще и для трейтов.

        Как вы будете использовать тайп-класс для типа — это уже ваше дело. Можно для бинарного сложения, а можно напрямую ТК дергать для своих целей.

        Возьмем, как пример, библиотеку circe для работы с JSON. Есть множество энкодеров/декодеров для примитивов из коробки. На основе этих энкодеров в circe есть дженерик для автоматического вывода энкодеров для любых классов (если есть необходимый). Т.е. в вашем кейсе достаточно было один раз описать свои энкодеры для необходимых примитивов и дальше использовать их сколько душе угодно. И никакого абуза макросов.

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

        Для L-уровня имхо лучше использовать type level подход просто по той причине, что это сопровождать будет проще.


        1. DanyMariaLee
          17.12.2016 17:08

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

          В холиварах я не мастер и спорить на тему эту смысла не вижу. В любом инструменте главное применение, будь это язык или какая-то его особенность/библиотека.
          У макросов достаточно вариантов использования, каждый выбирает свой в зависимости от задачи. Тут же тоже вопрос удобства возникает: вам надо подняться с первого этажа на пятый — можно пешком, можно на лифте.


        1. Optik
          18.12.2016 11:24

          1) Реализация на уровне типов в скале возможна далеко не всегда. К сожалению сейчас эта система типов не тьюринг полная.
          2) Помимо функциональных различий есть еще различия в производительности. Обобщенное программирование всегда будет выливаться в вызов виртуальных методов.

          А в целом да, холиварная тема.

          з.ы. цирцея все-таки с макросами живет.


          1. senia
            18.12.2016 12:44
            +1

            К сожалению сейчас эта система типов не тьюринг полная.

            Разве? Кажется вполне полная. Даже доказательства гуглятся, например.

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


            1. Optik
              18.12.2016 12:58

              Если правильно помню, то в этом было одно из основных отличий DOT. Пойду перепроверю.


          1. ImLiar
            19.12.2016 01:29

            AFAIR в circe макрос только для проверки, что можно вывести энкодер. А сам вывод на старом добром shapeless


  1. fogone
    17.12.2016 12:29
    -2

    Подскажите, пожалуйста, какую собственно проблему вы решали? Если уж вы взялись писать предметную статью, а не просто про макросы, то наверное стоило написать, вот стандартный api клиента аэроспайка предоставляет вот такой способ положить вот такое вот значение, это неудобно потому что нам нужно здесь передавать вот это и вот это, создавать то и то. Давайте попробуем упростить использование с помощью скала (или: сделаем удобный для использования из scala dsl, решающий эти проблемы), мы выбрали использование макросов, так как все остальные (нормальные) способы (вот такие..) мы исчерпали и они оказались неудобными/слишкоммедленными/подставьсвоё. Тогда нам ничего не оставалось как написать кодогенерацию, что позволило нам то и то.

    Сейчас же статья выглядит так: я решила поиграться с макросами скалы и написала кучу совершенно нечитаемого кода, чтобы решить сама не знаю какую точно задачу, но я о ней думаю как «впереди довольно много однотипных преобразований. Писать все это руками каждый раз желания нет.» Хочу заметить, что вполне возможно получившееся решение действительно решает какую-то проблему и даже возможно это действительно лучший способ решения этой проблемы, но из статьи этого понять невозможно, а разобраться в исходниках сложно даже имея некоторое желание — во многом как раз из за использования макросов (а совсем не потому, что из комментариев есть только имя автора).

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


    1. ImLiar
      17.12.2016 12:36
      +2

      Ну это уже звучит как трешталк неосилятора.

      Все там читаемо.
      И на 100% процентов уверен, что на _ЛЮБОМ_ языке, который человеку не знаком, можно найти такой пример, что у субъекта прикипит и он начнет исходить слюнями. От асма до name-yourself.

      Макросы в скале в текущей реализации — это экспоуз компилятор наружу, за что их не особо любят (это же, о боже, надо понимать, что такое Expr и Tree!).
      Позже появились квазицитаты, стало попроще. Сейчас пилят scala-meta, там все радужно и с пони.

      Но меня всегда умиляло, как вылезают такие уникумы, и на абстрактных примерах использования макросов или type level программирования составляют суждение о языке как таковом. Это все равно что о крестах судить через призму шаблонов. Пфф


      1. roversochi
        28.03.2017 14:56

        Я сначала хочу найти подходящие моторы, у которых будет этот самый «запас» мощности, и при этом чтоб они были тихими и дешевыми :)


        1. ImLiar
          17.12.2016 13:46

          У вашего комментария была мысль?


          1. fogone
            17.12.2016 15:21
            -3

            А вы считаете, что есть смысл писать комментарий, если в нем смысла? Даже если в саму эту мысль закралось противоречие?


      1. fogone
        19.12.2016 13:37

        Я лишь задал вопрос о том, какую проблему решает кодогенерация (пожалуй что слишком резко, за что уже принес извинения ниже). Из дальнейшего общения ответа на этот вопрос я так и не смог получить, если вы на 100% поняли этот код, можете в двух словах рассказать, зачем нужна кодогенерация в этой библиотеке?


    1. DanyMariaLee
      17.12.2016 15:46

      Я понимаю Ваше замешательство. Цель статьи была показать, как макросы могут облегчить разработку.
      Насчет кода самой библиотеки — комментарии были вынесены мной в отдельную директорию специально, потому как на мой взгляд легче читать код, не отвлекаясь на текст. В кукбуке https://github.com/TinkoffCreditSystems/aerospike-scala/tree/master/cookbook я подробно описала, как именно использовать на примерах, максимально наглядно. Уверена, прочитав примеры и мое разъяснение вот тут https://gist.github.com/DanyMariaLee/2774f10e64e6aaff2e855061aa74a16c, часть, а может и все вопросы исчезнут.

      Если после прочтения останутся вопросы я буду рада на них ответить и помочь Вам разобраться.


      1. fogone
        17.12.2016 17:38
        +2

        Я прошу прощения, предыдущий комментарий был чрезмерно резкий, но по существу мне осталось непонятным какой профит планировался по сравнению с обычным клиентом. Я почитал описание по ссылкам, там подробно описано устройство и использование, но не совсем понятно, какую чистую выгоду мы получаем используя этот дсл, какой именно бойлерплейт код призван убрать этот проект? Я использовал java client и не припомню никаких особых проблем с переиспользованием кода. Так что если у вас найдется время ответить на этот вопрос, было бы очень интересно этот ответ услышать.


        1. DanyMariaLee
          17.12.2016 18:10

          Все в порядке, извинения приняты)

          Если коротко, у нас база, в которую можно положить что угодно любого типа. java client возвращает некий Record, в котором лежит интересный нам Map<String, Object>. Тип, конечно, съедобный, но не вкусный, согласитесь? Мне нравится возможность положить в одном месте в базу HList или case class Human и потом в любом другом месте (сервиса, приложения) его достать. То есть получить именно тот HLIst/Human, который я сохранила.
          Тут вы скажете, как же это мы получим и где гарантии? Гарантий нет. Как и в любом другом случае нет такой силы во Вселенной, которая могла бы помочь Вам или мне, или кому-то еще контролировать, что/как еще кладется по нашему ключу ВНЕ приложения. Но. Данная библиотека помогает, делая максимум работы за нас, позволяя нам в случае неприкосновенности без нашего ведома данных (то есть Вася, сидящий в соседнем отделе, не решит взять и переписать именно Ваш сэт и сохранить там вместо информации по книгам в библиотеке свою любимую песенку в любом доступном его фантазии виде) получить желаемый тип.
          Пользуясь этой оберткой Вы пишете грубо говоря:

          def saveBook(b: Book): Future[Unit] = ???
          def getBook(k: String): Future[Book] = ???

          и не разбираете сами руками как там внутри этого драйвера сохранена Ваша книга. Конечно, зная значение ключа.

          Я думала уже над тем, как обезопасить свои данные от Василиев всех мастей, вероятно можно посмотреть на настройки в самом Аероспайке и определенному пользователю выдавать права на тот или иной сет. В коде библиотеки в таком случае можно будет описать дополнительные настройки для служебных объектов (типа Writer и тп). В доработках уже достаточно большой список «хотелок» и если Вы работаете на scala с Аероспайком Ваши пожелания мне интересны и будет здорово, если Вы запишите их в issues.


          1. fogone
            17.12.2016 19:40

            Хорошо, нам для хранения абстрактно любого типа достаточно сериализовать значение и записать его по ключу. Как сериализовать это второй вопрос, можно в джейсон и в строку, можно в бинарный формат, можно свой написать который будет значения по мапе размазывать, в любом случае готовых решений много. Сложности связаны с тем, что аэроспайк ориентирован в первую очедь на хранение в более «колоночном» виде. Но никто не мешает хранить весь объект в одном bin-е в виде массива байт. Обертка для такой сериализации насколько я могу себе представить не требует какой-то кодогенерации. Именно это меня и сбивает с толку — я не могу понять, для чего в итоге используется кодогенерация.


            1. DanyMariaLee
              17.12.2016 19:42
              -1

              Хорошо. Вы предположим Джейсон руками разбираете или подключаете что-то чтоб "само" разобралось?
              Это то самое, что сделает само, но для данной базы.


              1. fogone
                17.12.2016 19:56

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


                1. DanyMariaLee
                  17.12.2016 20:21
                  -1

                  Какой именно момент Вы хотите описать при помощи готового решения? Что это за решение будет?


                  1. fogone
                    19.12.2016 10:50
                    +1

                    Сериализацию, видимо. Я как раз и пытаюсь понять для чего же здесь используется кодогенерация. Хотя я посмотрел на сериализацию для кейз-классов и она всё равно сделана через рефлекшн. А это как раз то место, где можно было бы использовать макросы для скорости работы. Но если не для этого, тогда для чего же она используется?


                    1. DanyMariaLee
                      19.12.2016 10:58

                      Если посмотрите чуть внимательнее, заметите, что этот код вызывается внутри макросов.


                      1. fogone
                        19.12.2016 13:41
                        +1

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


                        1. DanyMariaLee
                          19.12.2016 20:23

                          Во всех предыдущих комментариях изложено все, включая последний мой ответ про рефлекшен (Вы посмотрели внимательно, надеюсь). Тот факт, что Вам не довелось работать или разобраться с макросами не означает, что против их использования надо выступать.
                          Зря удалили комментарий, все равно отвечу: конечно, я поняла этот код, как иначе


                          1. fogone
                            19.12.2016 23:32
                            +1

                            Во всех предыдущих комментариях изложено все, включая последний мой ответ про рефлекшен (Вы посмотрели внимательно, надеюсь).

                            Но назвать то место, которое стало лучше от использования макросов, вы всё-таки не можете?
                            Тот факт, что Вам не довелось работать или разобраться с макросами

                            Почему вы сделали такой вывод? Я этого не говорил.
                            не означает, что против их использования надо выступать.

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

                            В каком смысле? На хабре нельзя удалить комментарий, насколько мне известно.


                            1. DanyMariaLee
                              20.12.2016 06:53
                              +1

                              Вот я комментарий не увидела, а Вы — информации в написанном тексте.
                              Повторять очевидные вещи утомительное занятие, ответы на все вопросы есть и местами не по одному разу в разных формах. Одного комментария про рефлекшен более, чем достаточно, чтобы удовлетворить даже самого некомпетентного человека. Но так как я невероятно терпелива и это блог компании, а не мой, попробую объяснить ещё один раз (числительное — ключевое слово).
                              Основная идея этой библиотеки в том, чтобы пользователь не задумывался о внутренней структуре данных аероспайка и о том, как из пришедших к нему данных вытащить искомое. Также не должен размышлять, как эти данные хранить, это тоже продумано за него. Плюс рефлексии в макросах достаточно понятен (очень надеюсь, что это так), также важно, что код для различных типов как скажем в шэйплиссе писать не надо, все имплиситные врапперы сгенерируются сами "по требованию" — то есть именно те, которые Вы намерены использовать, а именно: если Вы напишете, что ваш бин "some s" — будет сгенерирован только один инстанс враппера, один, а не сто (числительное тут не конкретное, это просто утрирование, хотя по сути, при написании подобной библиотеки можно убить достаточно много времени, прописывая все типы, о чем прекрасно написано в гисте, который я написала в самом начале — посмотрите ещё разок на пример под номером один, и если все ещё не будет понятен профит — посмотрите ещё два раза).
                              Если Вам все ещё не понятно прошу не писать тут больше комментариев, потому что весь скаловый букварь в приложении к этой статье совершенно не нужен.


            1. Optik
              17.12.2016 22:21

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