Postlar filtri


За всю жизнь реализовал, наверное, больше двух десятков вариаций батчилок (реализовал бы одну и сделал библиотекой, да каждый раз вокруг то 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 (там их много если рефрешить)

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


Мои страх и ненависть dan repost
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.

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


Random Rust Dev dan repost
Когда надоело говорить "боровить", говори "реквизировать"


Статическая типизация это добро для рефакторингов.

Поменял тут &[u8] на AlignedBytes, а затем просто компилировал и фиксил, пока не собралось.


Чот по привычке уже сижу в этой cli ветке, кстати, хотя пару команд у него уже есть и осталось только допиливать, мержанул, нафармил квадратиков :)

Хотя теперь ридми скорее всего врёт, да и нужно про остальное дописать будет как-нибудь. Но вы поняли, когда я это сделаю: попозже)


Переделал фантомные записи, чтобы они были валидными только для следующего поколения которое мы создаём, т.к. у меня не было никакого механизма по их удалению. Теперь когда фантом создаётся — его идентификатор сохраняется в коллекции, а при коммите/отмене поколения увеличиваем идентификатор последнего фантома до максимального из существующих.

Кое-как впихнул в query-запросы код который когда видит фантом с идентификатором меньше разрешённого — удаляет его.

Затем попытался в диффы подпихнуться, но там как-то сложно сильно всё оказалось, решил заодно итератор который ходит по записям RocksDB переписать, чтобы во-первых заиспользовать raw-итераторы, которые выдают &[u8] вместо Box для ключей/значений, во-вторых чтобы оно прозрачно у себя там проверяло фантомы эти и если нашло — то запомнило что его нужно удалить и выдало следующее значение.

Но не получилось, тесты свалились. Разбирался-разбирался, плюнул, решил диффы совсем переписать. Второй раз заиспользовал FSM/FSA подход для выделения чистой логики нетривиального алгоритма от конкретных кусков его выполнения (первый раз был для трансформаций). Забил на I/O, определился какое состояние у меня должно быть, сделал два енама: Action и Input и начал обрабатывать приходящий Input в зависимости от текущего состояния, выдавая в результате Action. И они простенькие довольно, вроде «установи курсор вот на такую запись, дай мне ключ который там нашёлся, а так же дай следующий изменённый ключ». Если интересно, можно откуда-то отсюда посмотреть https://github.com/anfivewer/diffbelt/blob/cc747e642a873cbc7ff8e3d86815d0ba1c806dd0/crates/diffbelt/src/raw_db/diff_logic/mod.rs#L245 , там по методам более или менее понятно, что происходит. Поленился только для будущего себя комментарий написать, почему всё именно так.

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

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

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

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

Ну и если всё сойдётся, то по фану реализуем эту штуковину для взаимодействия бота с телеграмом https://t.me/alexandersmind/521 , запилю бота к фиесте (давно данные не обновлял, кстати), может и антиспамного бота перепилю, а там посмотрим 🙂

#diffbelt


Это, кстати, в тему о том, что на не сильно большом количестве элементов лучше забыть о всяких больших O() и что вставка в начало/середину массива это O(N), ужас-ужас, нужно на связный список переделывать.

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


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

Написал три бенчмарка, надеюсь нигде не протупил с black_box() и оно ничего не оптимизировало, результаты там такие:


test bench_serialize ... bench: 2,825 ns/iter (+/- 65)
test bench_deserialize ... bench: 2,385 ns/iter (+/- 56)
test bench_deserialize_unchecked ... bench: 410 ns/iter (+/- 12)
test bench_move_1 ... bench: 44 ns/iter (+/- 0)
test bench_move_2 ... bench: 44 ns/iter (+/- 1)
test bench_move_3 ... bench: 44 ns/iter (+/- 0)
test bench_move_4 ... bench: 44 ns/iter (+/- 0)
test bench_move_5 ... bench: 44 ns/iter (+/- 0)
test bench_move_6 ... bench: 44 ns/iter (+/- 0)
test bench_move_7 ... bench: 44 ns/iter (+/- 0)


Я сериализировал/десериализировал структурку с массивом из 16 элементов, в каждом из которых два 64-битных числа, строка на 32 байта, 64 случайных байта. Итого получилось примерно 2 килобайта данных.

И сделал 7 штуковин, которые эти два килобайта перемещают внутри вектора на n байт влево. Как видим — это пыль совсем. Т.е. мы можем спокойно на всякий случай префиксовать данные местом под потенциальное выравнивание, а перед тем как нам это нужно читать — проверить, и если не повезло (чего по идее вообще не будет происходить в большей части случаев), то просто сместить. Возможно на больших объёмах оно становится хуже, но скорее всего не сильно значительно, да и если будет беспокоить — тогда и посмотрим/форкнем библиотечку, сделаем эту замену вектора на вектор пачек байт, предложим PR, все дела.

#rust


Теперь жить стало ещё страшнее. Думаю как бы вкорячить возможность просить из васм-кода делать запросы, само апи у меня сейчас JSON'овое, хочу приделать ещё flatbuffers, во-первых судя по flamegraph сэкономлю процентов эдак 10 на парсинге json'ов, во-вторых ещё сколько-то на base64, в-третьих оно покомпактнее будет, в-четвёртых схема всех методов более наглядно будет описана, в общем, выглядит как хорошая идея.

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

Смотрите, flatbuffers работает так: даём ему (либо он сам создаёт) пустой Vec, используем кодогенерированные структурки чтобы записать какие-то данные, передаём в сериализатор WIP-объект (выдаются после записей структур) финального узла и финализируем сериализацию. После этого у нас есть три вещи: Vec держащий байты, head: usize говорящий с какого байта идут сериализированные данные, len: usize говорящий сколько там байт этих данных.

Иногда мне приходится копировать эти данные (например, из памяти васма, к которой относительно сложно обращаться безопасно, но думаю позже наворочу ансейфа и местами перестану копировать, а всё-таки возьму указатель и напишу себе каких-нибудь обёрток, которые будут гарантировать, что не натворю делов; но пока время этих оптимизаций не пришло, по тому же flamegraph видны другие проблемы, да и я пока не распараллелился даже). Ну так вот, я и делал Vec::from(&buffer[head..(head+len)]), который брал эти сериализованные данные и помещал в новый Vec, красота.

Но у нас опять контры с выравниваниями получаются. При валидации/чтениях этих сериализованных данных flatbuffers может запаниковать, сказав что я дурачок и дал ему невыровненные байты. Т.е. был там какой-нибудь u64, требующий выравнивания в 8 байт, был head = 5 чтобы гарантировать, что адрес где находится этот head правильно выровнен, чтобы все остальные адреса были хорошими, а я взял и всё сломал, сдвинув на пару байт в сторону.

И принимая эти сериализированные данные в виде байт по сети, если я запишу их в обычный Vec — нет гарантий, что всё сойдётся. u8 требует выравнивания в 1, переопредилить его нельзя. Чисто в теории можно создать Vec со структурой в восемь байт, но работать с этим потом будет ну совсем неудобно.

Хотел по коду std найти, почему оно не сломалось у меня, но за пару минут не вышло, бросил. Вероятнее всего потому что текущий аллокатор да ещё и на 64-битной системе старается всё с 8-байтовым выравниваем создавать, не знаю.

Что делать? Не знаю, как это другие делают, но я пожалуй добавлю себе в утилитки структуру аля AlignedBytesData(Vec) (где ALIGN < 255, но на практике пока только 8 нужно, ибо во flatbuffers нет 128-битных чисел), которая будет делать следующее:

◾️ Если нужно скопировать сериализированные данные, то аллоцирует вектор с размером данных + ALIGN байт. Узнаёт адрес первого байта, записывает в первый байт ALIGN минус остаток от деления на ALIGN, с этого смещения и записывает байты.
◾️ Если нужно прочитать — читает первый байт, узнаёт смещение, отдаёт слайс начиная с него и до ALIGN минус смещение с конца.

Завтра ещё в чатике спрошу по этому поводу и возможно в 64-битной системе это лишний оверхед, но я ведь живу и в 32-битном васме, который вполне может взять и аллоцировать вектор на полуслове, в который я затем запишу данные и всё развалится.

UPD: эта схема не прокатит из-за реаллокаций памяти вектора, после которого смещение может оказаться сломано.

#rust


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

Теперь посплю и попробую собрать не тесты, а выполнение этого добра на реальных данных :)

#diffbelt


C map/filter оно сильно просто получилось, т.к. функция по сути одна, которая балково получает список появившихся/изменившихся/удалённых записей которые нужно обработать и выдаёт что нужно добавить/удалить в целевой коллекции. А с map/reduce сложно, у меня тут пять функций:

◾️ map, почти тот же map/filter, превращающий список изменений в список ключей в целевой коллекции с какими-то данными относительно него
◾️ тривиальная функция создания первоначального аккумулятора, получающая ключ-значение из целевой коллекции и как там ей нужно инициализирующая вектор байт с данными аккумулятора
◾️ reduce, получающая аккумулятор и список данных из результата map, мутирующая аккумулятор
◾️ опциональная функция merge аккумуляторов (по сути reduce аккумуляторов). Если она есть, то это разрешает параллелить весь процесс, делая reduce'ы разных пачек данных, а затем объединяя аккумуляторы между собой, чтобы получить итоговый. Если её нет — то на весь процесс агрегации будет создан один уникальный аккумулятор и всё проредьюсится с ним
◾️ apply, которая берёт финальный аккумулятор и решает что записать в значение в целевой коллекции/удалить его

Решил, что лично я когда буду использовать эту штуку не буду гореть желанием описывать множество тестов, которые определяют полмира («есть вот такие-то изменения в изначальной коллекции», «есть такие-то записи в конечной», «вот такой должна стать конечная коллекция»), т.к. во-первых чтобы всякие кейсы разные потестить — это нужно будет много экранов копипасты делать, во-вторых обычно ошибки не в том как мы это всё объединили, а в самих шагах. Так что пока делаю возможность описывать тесты для каждой из этих функций в отдельности. Первую уже тестирую, вторую сегодня закончу. Дальше должно будет бодрее пойти, оно не сильно прям чтобы сложно было. Самое сложное — что нужно код сериализации/десериализации моих тестовых данных писать, а не тестировать 🙂

Как закончу, посмотрю, насколько сложно будет переиспользовать написанное, чтобы цепочку из этого добра составить, может и возможность определения интеграционного теста тоже добавлю. Будет полезно, ибо эти yaml-тесты из примеров выполняются как тесты проекта.

Я по приколу запустил измерение покрытия, кстати, удивился, там чуть больше 50% оказалось, если правильно смотрел. Но оно и не удивительно, этот звездолёт сильно сложно было бы просто на глаз делать и верить, что после каких-то доработок оно не выдаёт мусор и не удаляет все данные из базы :). А с TDD цикл между написанием кода и получением результатов его работы довольно неплох, удобнее чем какую-то программу целиком запускать и смотреть что она наделала.

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

Но чтобы оно действительно что-то делало, нужно будет ещё реализовать возможность из wasm-кода просить загрузить ключи вокруг определённых ключей, делать фантомные записи, чтобы иметь возможность реализовать подсчёт перцентилей имеющимися методами map/filter, map/reduce ( https://t.me/alexandersmind/482 , https://t.me/alexandersmind/483 ). А затем и нативно их поддержать, чтобы не приходилось этот бойлерплейт реализовывать в васме/шустрее работало. Надеюсь хоть к концу этого года допишу до результата который за два месяца на ноде накидал >_<

#diffbelt


Давайте расскажу, какие сейчас вообще дела. Мигрировал с wasmer на wasmtime (wasm-рантаймы), психанул, хоть и проблема была всё-таки в моём коде, неправильно глянул на документацию Vec::reserve(), передал туда меньше чем нужно, затем словил такие страшные спецэффекты, что вообще не понимал, что происходит, т.к. программа то выдавала мусор, то ломалась от добавления format!(), в общем, мрак. В процессе дебага попробовал повызывать свой васм из браузера, вполне себе получилось и там ничего не крешилось (но как оказалось я неправильно потестил, функция ломалась на втором вызове), списал всё на рантайм и сменил.

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

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

Сейчас занимаюсь тестовым фреймворком пользовательских агрегаций. Для map/filter операции оно уже есть, там описываешь что хочешь превращать одну коллекцию в другую, говоришь путь до wasm-файла, какие функции из него нужно использовать, ну и оно работает. Но wasm нетривиально тестировать, поэтому в yaml-файле конфига можно задать секцию тестов, где говоришь «давай прогоним вон ту трансформацию на вот таких вот данных, вот такой результат должен получиться». Дополнительно дореализовываешь функций которые умеют сериализированные входные/выходные данные в байты превращать и наоборот (либо делать noop, если данные строковые), запускаешь, оно шух-шух и прям как полагается пишет OK/FAIL на каждый тест и говорит что разошлось (даже диффалку прикрутил, красивое).

#diffbelt




Ну и другая чуть меньшая боль у меня была, когда нужно было использовать bytemuck, у которого есть derive который валидирует, что #[repr(C)] структура хорошая и её можно побайтово копировать в памяти и ничего страшного не произойдёт (мне нужно из хоста в васм и наоборот тривиальные структурки гонять). И всё было хорошо, пока я не попробовал этот derive накинуть на дженерик структуру. Оно сказало что такое не умеет, добавляй #[repr(C, packed)], а то не поверю, что там паддингов нет. Добавил, но теперь думаю таки убрать, ибо проблемы создаёт, а я вот стопроцентно не параметризирую эту структуру чем-то, что сделало бы паддинги, но никак не могу это компилятору/библиотечке доказать. Пока писал, тоже придумал, что могу рядом с этой структурой объявить модуль тестов в котором определю эту же структуру, но с конкретными типами которые я туда подставляю (их два всего) и с derive-валидацией этой, которая сборку бы уронила если бы такое нельзя было бы использовать, а трейты unsafe'но реализую, ибо «зуб даю, оно работает». Мне интересно, кстати, что мне нужно будет делать, если я захочу архитектуру с big-endian поддержать (тогда хост будет BE, а wasm продолжит быть LE), но это уже совсем другая история.

#rust #мужчина_вы_что_не_видите_у_нас_обед


У меня иногда возникает состояние, которое я называю «Rust Fatigue».

Это когда тебе нужно что-то сделать, для тебя оно выглядит довольно логичным и простым, но в расте... это сделать почему-то слишком сложно. Либо легко, но... только в nightly.

Ситуация: мне не зашли растовые библиотеки парсинга yaml, ибо половина кривые и чего-нибудь да не поддерживают, половина под serde, где местами сложно парсить так как хочешь, что в итоге структуру yaml проще поправить, чем написать парсер того, каким хочешь этот yaml видеть. В итоге я взял unsafe-libyaml, которая является трансляцией сишной libyaml и поддерживает всё что мне нужно, сделал обёртку, всё ок (из фич что мне были нужны — возможность знать строку и колонку любого элемента, чтобы нормально ругаться на формат/ошибки конфига; плюс возможность распарситься в «дай мне чистую ноду, я сам по ней руками пройдусь»). Затем я решил сделать обёртку вокруг сериализации, чтобы в тестах можно было сравнивать expected результат описанный в ямле с тем что просериализую из actual результата.

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

Ну и я думаю: понятия не имею, куда эти байты захочу складывать, приму в функцию сериализации &mut impl Write, а там что передадут — туда и запишу. Но в качестве контекста коллбека выдачи итоговых байт мне нужно передать *mut libc::c_void, что простой указатель. Ну а &mut impl Write это один указатель плюс известный в компайлтайме тип. Я и думаю: кастану в &mut dyn Write, сделаю структурку, в которую мы положим два указателя — один на инстанс, другой на vtable или чего там нужно, чтобы вызывать методы у этого трейта.

Но не тут то было, нет сейчас явной возможности получить указатель на vtable, это nightly фича ptr_metadata которую делают с 2018 года. Вот так вот можно: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=d43d5275be1c6669de2a5d95f8d84c35

А в stable так нельзя и непонятно, чем именно заменить то. Пока писал этот пост подумалось, что нужно посмотреть, можно ли сконвертить impl Fn() в указатель на эту функцию, тогда по идее можно было бы сделать замыкание, которое умеет вызывать что нужно, указатель на замыкание был бы одним указателем и возможно сойдётся, но не факт. Кажется что не сойдётся, ибо тип замыкания это тоже странная штука довольно.

Попробую вечерком ещё поиграться с функциями, но если ничего не получится, то придётся таки принимать не &mut impl Write, а просто Vec и/или String, ну и зная конкретный тип там уже без проблем всё кастуется. И для каждого типа который захочется использовать — придётся либо копипастить, либо макросами обмазываться, раст победа.

#rust #мужчина_вы_что_не_видите_у_нас_обед


Пишу сейчас код, который собирается в wasm, насобирал чутка лайфхаков.

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

В пакете общих утилит сделал тип Annotated, у которого #[repr(transparent)] (т.е. по сути это алиас для типа и в итоговой сборке ничего от него не останется). Им размечаю указатели, чтобы было понятнее, что за ними должно лежать.

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

А затем можно сделать impl на таким образом аннотированный указатель, который возьмёт и всё распарсит, красота.

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



20 ta oxirgi post ko‘rsatilgan.