Фильтр публикаций


Вряд ли я домотаю до поста где я только начинал с объяснениями, зачем я захотел сделать эту штуку, для тех кто недавно подписался: изначальную проблему я встретил в платформе с телематическими данными от автомобилей. В машинах есть gps-трекеры, они присылают сообщения с координатами, значениями датчиков. Это «сырые данные», в моей концепции мы их должны просто класть в изначальную коллекцию.

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

Затем мы хотим иметь разные уведомления, например, что длительность поездки превысила 3 часа, или что получили сообщение с координатами в определённой геозоне. Либо более сложные — определение нарушения правил ЕСТР (есть ограничения сколько можно ехать и минимум времени который нужно отдыхать). В моей концепции они так же попадают в новую коллекцию уведомлений исходя из того что навычисляли в промежуточных.

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

А теперь plot twist — в конце месяца пользователь получает отчёт, где машина, судя по пробегу, съездила на луну, потому что GPS половину месяца уносило непонятно куда. И определение поездок он криво настроил. В итоге он берёт, меняет настройки, удаляет/редактирует кривые сообщения, просит переделать отчёт. Здесь мы ловим проблему инвалидации кеша. Мы точно предпросчитывали какие-то данные, чтобы строить отчёты быстрее, т.к. люди жаловались, что они слишком долгие. Какие из этих данных нужно выбросить?

Фиг его знает. А мне хочется всегда знать зависимости между данными, выкидывать как можно меньше кешей и если это возможно — пересчитать только изменения. Поменяли настройки — не повезло, придётся пройтись по всему что есть и подкорректировать, а затем по всему дереву далее. Удалили пару сообщений — повезло, посмотрим в интервал куда они попали и соседние, разделим/склеим, изменения распространятся по дереву выше так же только в местах где что-то поменялось.

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

Вот что-то такое я хочу. Посмотрим, в общем, как пойдёт и дойдёт ли куда-нибудь 🙂

#diffbelt


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

Штука довольно прикольно выглядит. Я задумывал это как возможность писать интеграционные тесты, но по сути если назвать чуть иначе, то там уже практически можно демонов обработки этих потоков данных делать. Там уже доступен запрос «жди пока у коллекции Х не изменится id поколения», есть возможность попросить запустить трансформацию. Т.е. уже можно сделать цикл который следит за тем, что данные были изменены и чтобы оно пересчитало их в следующую коллекцию. Параллельно можно запустить ещё таких штук, которые будут следить за изменениями следующих коллекций и перекладывать дальше. Это и так будет отдельной командой (и по задумке оно будет умным, видя что таких команд запущено больше одной на разных хостах и будет делить работу в тех трансформациях где это возможно), но мне нравится, что это уже физически реализуемо.

Ну и на самом деле это полукостыль, потому что такой агент на самом деле должен выполняться не внутри васма, а быть самостоятельным бинарником, который просто ходит в апи (чтобы он мог и сайд-эффекты относительно внешнего мира совершать). Но сейчас для тестирования всего этого васм-взаимодействия полезнее такой вариант, уже отловил пачку заляп ансейфных (благо в дебаге оно даже в unchecked-операциях обложено проверками на валидность и довольно понятно говорит что пошло не так; а без unsafe там не получится, т.к. всё общение идёт через `extern "C"`).

Вряд ли на этой неделе закончу с proof-of-concept потокового вычисления перцентилей, там нужно разбираться в мёртвом коде трёхлетней давности. Но очень хочется доделать, даже если затем просто выкину. Выкину потому что не так уж они и нужны скорее всего. Ко мне пришла довольно логичная мысль, что для такого кейса использования проще использовать мою балалайку как source of truth относительно данных и затем имея механизмы обеспечения консистентных преобразований наладить синхронизацию части лежащих у меня данных с каким-нибудь clickhouse, где производить вычисления, где они определённо будут сильно быстрее из-за правильной укладки данных, а затем выдавать результаты в итоговые коллекции, где их заберёт тот кому они нужны. А всё что я делаю просто даст возможность всегда знать, актуальны ли данные лежащие в итоговой коллекции, насколько они отстают от исходных, иметь гарантии консистентности входа и выхода.

#diffbelt


Прокачал свой OwnedAlignedBytes этот, радуюсь RAII или около того. До этого передавал из васма через ffi указатель на структуру в которой лежат кишки из которых можно собрать Vec (указатель на него, капасити и длина), хост знал сколько байт нужно записать, вызывал у васма функцию которой передавал указатель на эту структуру, оно там в памяти васма восстанавливало вектор, подкручивало на сколько нужно capacity, деконструировало вектор обратно в эту структуру, хост записывал байты и менял length у структуры, передавало васму управление и он мог восстановить вектор в котором находятся нужные байты. После чего возможно нужно было двигать байты, чтобы выровнять для парсинга flatbuffers.

Теперь так: у OwnedAlignedBytes есть метод unsafe fn write_bytes(&mut self, len: usize) -> WriteAlignedBytes


Посмотрю как совсем нечего делать будет — https://www.youtube.com/watch?v=ArQNyOU1hyE

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

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


Пришлось немного пооптимизировать, чтобы wasm-модуль компилировался только один раз, забодало ждать секунд десять, пока тесты прогонятся (сейчас всё равно две секунды в дебаг-сборке, пробовал даже [profile.dev.package."*"] opt-level = 3, почему-то не помогает). Плюс добавил подсчёт времени на прогон кейса, красивое:


Loaded wasm file example in 137.924947ms
[ OK ] transforms.parsed_lines_1d.map > basic, 2.251365ms
[ OK ] transforms.parsed_lines_1d.map > empty, 59.34µs
[ OK ] transforms.parse_lines.map_filter > with props and no extras, 3.084386ms
[ OK ] transforms.parse_lines.map_filter > with props and one extra, 220.157µs
[ OK ] transforms.parse_lines.map_filter > with props and two extras, 146.782µs
[ OK ] transforms.parse_lines.map_filter > with no props and no extras, 127.173µs
[ OK ] transforms.parse_lines.map_filter > with no props and two extras, 134.486µs
[ OK ] transforms.parse_lines.map_filter > not matches start, 72.015µs
[ OK ] transforms.parse_lines.map_filter > with stack extra, 179.017µs
[ OK ] transforms.parsed_lines_1d.reduce > basic, 442.891µs
[ OK ] transforms.parsed_lines_1d.merge_accumulators > basic, 472.15µs
[ OK ] transforms.parsed_lines_1d.apply > basic, 350.193µs
[ OK ] transforms.parsed_lines_1d.apply > empty, 58.245µs
[ OK ] transforms.parsed_lines_1d.apply > mismatched count, 59.165µs
[ OK ] transforms.parsed_lines_1d.apply > negative count, 59.606µs
[ OK ] transforms.parsed_lines_1d.apply > mismatched negative count, 57.78µs
[ OK ] transforms.updateMs_1d_intermediate.map_filter > should not match, 800.988µs
[ OK ] transforms.updateMs_1d_intermediate.map_filter > should match, 263.128µs
[ OK ] transforms.parsed_lines_1d.initial_accumulator > empty, 82.186µs
[ OK ] transforms.parsed_lines_1d.initial_accumulator > few, 445.984µs


Никогда не мерял ещё скорость запуска какого-либо кода в микросекундах 😃

Стало далеко до плана листать, оставлю ссылку. Реализовал создание коллекций в базе из конфига, секцию в конфиге для интеграционных тестов, создание временной базы для такого, инициализацию её. Теперь нужно биндингов наделать нужных и чтобы оно работало, начать и закончить :). Единственное что кроме биндингов нужно будет и механизм вызова запросов из трансформаций реализовать, а то у меня только наброски на них есть (что-то пока висящее в воздухе не помню уже в каком состоянии, ну и кучка flatbuffers-типов всех нужных методов, которые пока даже не парсятся у сервера).

#diffbelt




Попал вчера в засаду. У меня был трейт:


trait FlatbuffersType + flatbuffers::Verifiable + 'fbb {}


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

И вдруг осознал, что мне всё не так нужно. Точнее что когда я сериализатор превращаю в OwnedSerialized> мне не нужен этот 'fbb больше, а нужен 'static, т.к. валидация сериализации уже не нужна, а валидность структур которые мы читаем легко получается т.к. у нас есть метод fn data SomeFlatbuffersType -> impl SomeTrait: FlatbuffersType и все методы которые принимают FlatType зависят от 'fbb.

Но как-то это нехорошо, пришлось слишком много кода переделывать, плюс теперь необходима кодогенерация. Я не могу реализовать этот FlatbuffersGenericType для всех типов которые у меня есть (даже если бы там не было метода `name()`). У прошлого трейта всё было просто:


impl + Verifiable + 'fbb> FlatbuffersType = $name


Реализовал первую поддержку и json, и flatbuffers параметров запроса. Теперь могу формализировать один из хаков для победы над лайфтаймами. Совсем простенький, но в начале не сразу было очевидно.

Кейс такой: нужно читать и json, и flatbuffers. Очевидно, для этого нужно создавать owned-структуры и хотелось бы создать третью, которая будет ссылаться на одну из структур (json/flatbuffers). Если просто втупую в разных ветках if'ов попарсить и сделать эту третью структуру — то борроу чекер обоснованно скажет, что структуры будут уничтожены в блоках if.

Но нет никаких проблем хранить эти структуры снаружи в Option. Сделал себе функцию даже:


pub fn store_in_option(opt: &mut Option, value: T) -> &T {
*opt = Some(value);
opt.as_ref().expect("just stored")
}


Тогда снаружи делаем let mut some_value = None на все значения которые нам нужно хранить во всех ветках, в ветках спокойно создаём их, пересохраняем let value = store_in_option(&mut some_value, value), свободно пользуемся полученной ссылкой, лайфтайм у неё будет как у some_value.

Раньше тоже так делал, но как-то не так сильно в глаза бросался приём.

#rust


Зашёл в доку flatbuffers, не узнал. В прошлом году не так модно выглядела.


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

UPD: о, что-то не ровно посчитал, похоже придётся срезать с правого конца немного(
UPD2: да и внизу чуть больше вышло, как так то, там не спилить уже. Надеюсь если первая норм будет, то вторую уже с исправлениями замоделирую :). Точнее я пока всё равно внутренние шестиугольники печатаю группами, нужно будет рамку под них подогнать, отцентрировав соты.


До этого я моделировал максимум карты для Counter-Strike в далёком детстве и какие-то домики в SketchUp, не помню уже зачем.

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

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

С каждым ограничением число степеней свободы уменьшается и двигая что-нибудь видим, что ещё нужно ограничить. Пока не получим зелёненькое Fully constrained. Меняя размеры/углы оно автоматически перестраивается как нужно.

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

Если бы интерфейсы верстались такими штуками, то жизнь бы стала налаживаться :)


Про то как мне понравилось 3D-моделирование всё-таки позже расскажу, спать пора 🙂


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

#diffbelt


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

Таким образом оно будет не только тестировать корректность wasm'а, но и корректность самих трансформаций и работу базы. Такие тесты всё равно когда-нибудь нужно будет писать (у меня есть тесты на базу, которые её запускают целиком и проверяют что методы работают как нужно, есть отдельные тесты на прогон транформаций (но без запущенной базы), а вот будут и полные, которые прогоняют трансформации на живой базе). И тут сильно меньше работы получается.

Всё что будет нужно — писать human-readable функции для коллекций (они пригодятся в будущем, когда буду UI делать, чтобы глазами на коллекцию смотреть) и... всё. Тесты эти будут писаться wasm-функцией, в итоге она сможет переиспользовать механизм запросов который будет доступен из трансформаций, единственное что нужно сделать кроме — дополнительную функцию запуска трансформаций, но это тривиально. И не нужно писать human-readable функций для промежуточных типов данных, они будут прятаться внутри вычислений и проверяться только итоговый выход (и его тоже проще будет проверять, т.к. не нужно будет сериализировать-десериализировать их туда-сюда, а просто сравнить либо побайтово, либо структурно, смотря что там за значения лежат в коллекциях.

Не уверен что найду сильно много вечеров/выходных чтобы плотно этим всем заняться, я тут ещё и Path of Exile 2 скачал, плюс новый DLC Factorio всё ещё не пройден, но по чуть-чуть постараюсь возвращаться.

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

◾️ Написать flatbuffers-схемы запросов которые могут понадобиться для тестов (старт поколения, запись значений в коллекцию, коммит поколения, чтение коллекции, по сути и всё, наверное и взятие диффа между поколениями не нужно пока)
◾️ Реализовать в cli команду инициализации базы согласно определённым в конфиге коллекциям (странно, я думал она была)
◾️ Дополнить парсинг конфига, при виде специального типа тестов создавать временный каталог, стартовать новую базу в нём, создавать коллекции
◾️ Сделать новый wasm-биндинг запуска трансформации по имени из конфига (такая команда в cli уже есть, нужно просто вызвать нужную функцию)
◾️ Написать самих тестов, которые создадут новые записи в изначальной коллекции, попросят запустить агрегацию, прочитают итоговую, сравнят с ожидаемыми значениями, промутируют коллекцию, снова запустят, посмотрят что обработка диффа тоже работает
◾️ Тесты ожидаемо упадут, т.к. у меня не реализована пока сама логика этих агрегаций, реализовать их
◾️ Всё ещё будут падать, т.к. не реализован механизм запросов, реализовать их (нужно будет помимо json-апи поддержать и flatbuffers)
◾️ Должно будет сойтись

Ну а когда всё-таки осилим это, сделаю cli-команд для чтения коллекций и перейду к реализации взаимодействия с телеграм-ботами по схеме https://t.me/alexandersmind/521 , т.к. у меня помер антиспам-бот (форк Shieldy с фичей кастомных вопросов вместо каптчи) и не особо хочу его чинить (там что-то мутное с нарушением уникальных ключей в монге, я так и не понял). Реализую его новую версию, выкинув все фичи кроме совсем необходимых, запущу, посмотрю, нужен ли он ещё кому-нибудь. Он будет генерировать какое-то количество метрик (ничего криминального, никаких сообщений не логирую, только количества, тайминги), которые пообрабатываю всей этой ерундовиной. Только wasm попробую не на расте писать, хочу Zig ещё попробовать, а воркер на Go бахну, давно на него не смотрел и кажется подойдёт для того чтобы без конца ходить в апишку за изменениями, парсить и класть задачи в коллекции, которые затем будут обрабатываться трансформациями.

#diffbelt


9 месяцев не было #diffbelt, жесть. Ничего практически не делал, словил какой-то сильный fatigue относительно него. А потом переезд, покупка 3D-принтера и как-то отложилось. Про 3D в следующем посте немного напишу.

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


- name: parsed-log-lines:1d
human_readable:
wasm: example
key_to_bytes: parsedLogLines1dKeyToBytes
bytes_to_key: parsedLogLines1dBytesToKey
value_to_bytes: parsedLogLines1dValueToBytes
bytes_to_value: parsedLogLines1dBytesToValue


Затем описываешь транформации, например:


- name: parsed_lines_1d
source: parsed-log-lines
target: parsed-log-lines:1d
reader_name: parsed-log-lines
aggregate:
wasm: example
map: aggregateMap
initial_accumulator: aggregateInitialAccumulator
reduce: aggregateReduce
merge_accumulators: aggregateMergeAccumulators
apply: aggregateApply
human_readable:
wasm: example
bytes_to_target_key: parsedLogLinesBytesToTargetKey
bytes_to_mapped_value: parsedLogLinesBytesToMappedValue
mapped_value_to_bytes: parsedLogLinesBytesToMappedValue
bytes_to_accumulator: parsedLogLinesBytesToAccumulator
accumulator_to_bytes: parsedLogLinesAccumulatorToBytes


Где ещё кучка таких функций, а map/initial_accumulator/reduce/merge_accumulators/apply реализуют саму логику агрегации и работают с байтами.

И затем идёт секция с тестами, где для каждой из функций можно накидать значения на вход и ожидаемый выход. Которые распарсятся, превратятся в байты, подёргают wasm-функций, получат результат, сериализируются в строки и сравнятся с ожидаемым. Для map/reduce это довольно тривиальная задача и требует только чтобы были определены у самих коллекций функции человекочитаемости. А для агрегаций это ещё вот пачка функций которые нужно написать.

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


За всю жизнь реализовал, наверное, больше двух десятков вариаций батчилок (реализовал бы одну и сделал библиотекой, да каждый раз вокруг то mobx, то redux-thunk, то redux-saga, то effector вообще) — классов, которые параметризируются чем-то, что берёт из очереди сколько нужно задач, выполняет их пачкой (делает запрос, например) и выдаёт результат для каждой задачи отдельно. С разными вариациями в виде настроек времени между пачками, throttle/debounce для сбора пачки, поддержки отмены задач, объединения одинаковых задач добавленных несколько раз по уникальным ключам, кастомизации сбора пачки в случаях когда нельзя в одну пачку складывать данные разных категорий (и не хочется делать для них отдельные экземпляры очередей), может ещё что-то было.

И мне нравится когда в реакте ты можешь просто в месте где тебе нужны какие-то данные вызвать хук/обернуть компонентом и когда оно отрендерится (и, опционально, будет видно на экране) — добавит задачу в очередь, множество таких мест на экране соберутся в одну пачку, игнорируя дубли, сделают один запрос и покажут нужные данные.

Как мне кажется это более правильный путь, чем пытаться во всех местах где мы получаем данные искать места где доступен весь список сущностей и делать один запрос там, без сложной машинерии очередей и группировки. Особенно не хочется думать о том, что делать, если часть исходных данных поменяется и нужно повторить запрос только для изменённых сущностей, придётся делать неизвестно что (чаще всего проще забить и перезапросить вообще всё), а в варианте с одиночными запросами всё просто — зависимости изменились, снова добавились в очередь, автоматически объединились с прочими запросами и запросили только то, что нужно.

#фронтенд #react


https://www.architecturalkatas.com/about.html
https://www.architecturalkatas.com/rules.html
https://www.architecturalkatas.com/kata.html (там их много если рефрешить)

Это они прикольно придумали 🙂


Репост из: Мои страх и ненависть
https://cantunsee.space/

Микроквиз на умение искать проблемы дизайна. Делитесь результатами


Если вдруг у вас где-то запущен SOCKS5, а вам нужен SOCKS4, либо вообще хотите даунгрейднуть его до HTTP-прокси (или не особо хотите, но там где вы хотите его использовать просят такой), то есть тула gost которая без проблем собирается в один go build и которой можно делать так:


gost -L http://127.0.0.1:8888 -F=socks5://user:password@12.34.56.78:8090


Что сделает локальный HTTP-прокси на 8888 порту через socks-прокси где-то далеко (и там целая куча других вариантов и можно использовать в разных комбинациях).


Когда услышал про regreSSHion и нет ключей от своего серверочка, а подключение говорит remote software version OpenSSH_8.9p1.

Надеюсь не поздно :)

Показано 20 последних публикаций.