Найти тему
Дошёл до реализации курсоров, интересная проблема вылезла, которой не было в прототипе (точнее была, но там я не думал об этом). Есть возможность вычитать коллекцию целиком: делаешь запрос query(), он тебе возвращает пачку данных и cursorId, с которым идёшь в readQueryCursor(), который возвращает следующую пачку с новым курсором и так пока не прочитаешь всё. У пачек есть лимит на количество элементов, вроде бы хорошо. Но что если наш запрос может иметь такие аргументы, которые не выбирают ни строки из всей коллекции? Например, у нас сколько-то миллионов записей, а запрос просит выбрать те, которые существовали в версии X, где записей было всего пять штук (предположим, что индексов для таких запросов у меня нет (хотя и на самом деле нет и не будет, я предполагаю такие юзкейсы, где история будет быстро отмирать, оставляя только актуальную версию)). Ну и вот мы бежим такие по коллекции и пытаемся собрать пачку из 200 элементов (наш лимит). Долго бежим, а элементов всё нет и нет. А если клиент ошибся в своих скриптах и прислал нам сотни таких запросов (хотя и десятка хватит)? Можно ввести таймауты, ограничить количество одновременных запросов. Но теперь я смотрю на это так, что страдать должен клиент, а не база. У API чего-либо не должно существовать методов которые вернут результат «ну как будет готово, так и вернём, может 50мс, может пять минут, может час, мы не знаем». Методы должны всегда отвечать быстро, если же не могут, то говорить «мы приняли ваш заказ, ожидайте» (а затем событием уведомить о готовности). Второй вариант это усложнение, радикализируем первый. Говорим, что чтение курсора может вернуть... ноль элементов, но если есть идентификатор следующего курсора — там возможно есть что-то ещё. А внутри собираем пачку не только по лимиту элементов, но и считаем, сколько элементов мы уже посмотрели. Если глянули уже, например, тыщ десять, то сворачиваемся, возвращаем что есть, запоминаем в новый курсор место где остановились. В итоге наш метод ограничен по времени выполнения сверху и будет стабильно возвращать какой-то результат. Что во-первых не требует введения таймаутов (что сложно, т.к. нужно будет потом иногда смотреть, а не отменили ли нашу задачу), во-вторых если нам придёт сотня-тысяча запросов на такие выборки, то мы будем их быстренько разгребать, параллельно обрабатывая и другие запросы, а не всё больше увязать в бесконечных переборах, которые очень быстро забьют все потоки намертво. #diffbelt
2 года назад
Как сломать RocksDB (точнее приложение, которое его использует): определяем компаратор для ключей, который умеет крашить процесс. Просим добавить плохой ключ в базу. Всё упало. Фиксим бажину, которая пишет не те ключи не туда. Всё равно не можем запуститься, потому что после открытия базы оно видит, что есть ключи которые нужно дописать, сравнивает снова и падает. Из-за чего затем приходится удалить базу и начать заново. Варианта сделать так, чтобы при невалидных ключах оно возвращало ошибку записи я не вижу, похоже что в колонки, где есть компараторы буду при инициализации записывать ключ с пустой строкой, а при сравнениях делать вид, что невалидные ключи равны пустой строке/любому другому невалидному значению. Тогда значения будут к этой пустой строке и записываться. Я проверил, что даже если в WriteBatch удалять старый ключ и добавлять новый, но при это компаратор говорит что они равны — то он останется старым. Ну и логировать это, плюс отправлять сигнал, что процесс нужно закрешить (уже вне сравнений), т.к. мы явно данные где-то теряем, так нельзя работать в любом случае. #rocksdb
2 года назад
Написал уже что-то около трёх тысяч строк кода на #rust, довольно бодренько идёт, уже допилил get/put/putMany методы, коммиты в ручных и автоматических коллекциях, осталось завезти хранение reader'ов, курсоры на выборки и разницы, сборку мусора. Курсоры по разницам будут самыми сложными, но думаю ещё за недельку-другую допилю, а там уже можно будет бенчмарков каких-нибудь сообразить и смотреть, так ли страшны эти RwLock'и. Больше всего напрягает, что если есть кучка типов-обёрток (например, struct GenerationId(& [u8])), которые вводишь чтобы можно было во-первых им методов добавлять, во-вторых чтобы случайно не присвоить одни байты в другие байты (например, CollectionKey в GenerationId), то потом сильно много бойлерплейта приходится им писать. Например, есть у меня тип RecordKey(& [u8]), который представляет собой ключ который я использую для записей в RocksDB, у него есть конструктор RecordKey::validate(& [u8]) -> Result<RecordKey, Error>, проверяющий, что байты валидные (там хранится три куска с их размерами, нужно проверить, что размеры не больше чем есть байтов и что лишних нет). Есть метод .to_owned() -> OwnedRecordKey, который делает копию этих байтов. Теперь у нас два типа, делающие одно и то же, но один владеет байтами, а другой просто оборачивает ссылку на них, чтобы методы вызывать на этих байтах. Я был бы рад иметь только OwnedRecordKey, но если я откуда-то получил голые байты и мне нужно их только прочитать, то я не смогу использовать методы из OwnedRecordKey без его конструирования, которое потребует копирования. По-хорошему сделать бы trait IsRecordKey, в котором определю методы (у меня это getCollectionKey(), getGenerationId(), getPhantomId()), а затем реализовать его для OwnedRecordKey и RecordKey. Но это просто тонна дублирующегося кода на ровном месте, который отличается только наличием & в варианте у OwnedRecordKey. В итоге приходится реализовывать методы только у RecordKey, а когда нужно использовать их от OwnedRecordKey, то делать .as_ref(), получать экземпляр RecordKey и использовать их от него. Ну и ладно, когда таких типов две штуки и если там действительно логика какая-то есть. Но когда это совсем тупые обёртки (как в изначальном примере с GenerationId), а у тебя есть ещё три таких, надоедает им методы дописывать для кастований туда-сюда в разные их виды (владеющий, ссылочный, доставания байтов из него, взятия байтов для чтения, взятия байтов для записи). Я чувствую, что нужно разобраться с макросами, чтобы я мог просто в #[derive(...)] своих Trait'ов дописать и оно там шух-шух-шух, нагенерило мне всего. Но как-то сложновато оно пока выглядит, приходится ныть и продожать плакать, колоться, но жрать кактус.
2 года назад
Накидал целую кучу лайфтаймов и смог вернуть из функции своё первое юзабельное замыкание использующее ссылки, а не Arc/Rc/перемещение значения. На данный момент я понимаю это так: ◾️ Есть время жизни 'a, все ссылки которые я передаю в эту функцию будут столько жить (кроме generation_id, он просто мне в замыкании не нужен и не трогал его лайфтайм) ◾️ Функция возвращает замыкание, которое тоже имеет время жизни 'a (хотя я пока не уверен, что правильно понимаю этот плюс; отдельно что такое dyn вроде как понятно, отдельно лайфтайм более или менее понятен, а что там куда плюсуется пока не очень) ◾️ Соответственно внутри этого замыкания можно без проблем продолжать пользоваться ссылками &'a, они будут валидны одно и то же время, замыкание не переживёт данные, у которых одолжили ссылки ◾️ Затем я вызываю эту функцию в функции рядышком, где не теряю владение над данными (не деструктурирую) до момента как деструктурирую замыкание — и всё сходится ... сейчас, правда, перепишу половину, т.к. как часто бывает, пока писал сюда портянку, зачем оно вообще сделано, понял, что можно упростить и не в двух местах блокировки держать, а в одном и тогда параметризация эта не нужна. А в общем смысл такой, что у меня при записи ключей есть опция ifNotPresent: true, которая если включена, то если попросят добавить два значения с одинаковыми ключами одновременно, то нужно записать только одно и вернуть в результате, записалось оно или нет. При этом можно игнорировать случаи, когда происходят одновременные записи с таким параметром и без него (если наврём про то что он записался, то когда к нам придут можем сказать, что он то записался, но запрос без опции обрабатывался вторым и перетёр). #rust
2 года назад
В ответ на пост Сейчас выяснил, что RwLock<RefCell<Option<...>>> это бред полный. Я почему-то думал, что нельзя просто так изменять значения в структурах (не знаю, почему) и нужен этот RefCell, который позволяет заменить значение. А он на самом деле нужен для того, чтобы когда у тебя есть ссылка для только чтения, то ты мог в это поле всё равно что-то записать: struct Example { field_a: i32, field_b: std::cell::RefCell<i32>, } #[test] fn example_test() { // Modifications of `field_b` will work even without "mut" there let mut example = Example { field_a: 42, field_b: std::cell::RefCell::new(13), }; let example_ref = &example; example_ref.field_b.replace(2); let example_mut_ref = &mut example; example_mut_ref.field_a = 1; assert_eq!(example.field_a, 1); assert_eq!(example.field_b.borrow().clone(), 2); } Плюс RefCell не поддерживает многопоточность. А RwLock и так даёт брать &mut ссылки на его содержимое, плюс Option оказывается имеет методы для его мутации: .take() чтобы забрать значение и оставить None, .insert(x) для замены на Some(x), .replace(x) для замены на Some(x) с возвратом того, что там было. #rust
2 года назад
А ещё кажется, что как закончу с переносом реализации, который делаю по принципу «пиши как можешь, лишь бы доделалось» придётся сесть и выкинуть половину Arc<RwLock<...>> которые я тыкаю ибо его просто добавить и оно работает. Многопоточность там взялась только из-за... HTTP-интерфейса. Чувствую, что это ерунда нейкая, по-хорошему мне бы сделать пул потоков в которых будет всё без блокировок, сделать один контролирующий поток куда сообщениями закидывать клиентские запросы, там разруливать, можно ли что-то делать одновременно и оттуда раскидывать задачи по пулу, одновременно с актуализацией данных в тех потоках куда отправляем. «Но потом, не сейчас» #rust
2 года назад
Страшно, вырубай. Написал какую-то такую ерунду чтобы передать замыкание и чтобы в нём были доступны нужные данные положил там их рядышком, чтобы тот, кто вызывает эту функцию мне их передал сам, т.к. я не понял, как доказать расту, что я внутри замыкания ссылочки возьму и всё сойдётся, не будет никто его вызывать после момента как ссылка станет невалидной. А потом понял, что с тем же успехом я могу создать структуру вроде: pub struct DatabaseInner { collections: Arc<std::sync::RwLock<HashMap<String, Collection>>>, } Накинуть ей реализации: impl DatabaseInner { pub fn get_reader_generation_id( &self, collection_id: &str, reader_id: &str, ) -> Result<GenerationId, GetReaderGenerationIdFnError> { let collections = self.collections.read().unwrap(); todo!(); Err(GetReaderGenerationIdFnError::NoSuchCollection) } } А потом эту структурку передавать в дочерние элементы и не использовать замыкания в принципе (хотя по сути получилось оно же, но в явном виде, как и изначальный вариант). Но с lifetime'ами ссылок нужно разобраться, может можно было и замыкание собрать. #rust
2 года назад
Сделал себе RwLock<RefCell<Option<CollectionGeneration>>>, и почему-то не смог вызвать .borrow() после .read().await. Сначала подумал, может это с tokio::sync такие дела, а std::sync таки нормально на это реагирует, но не. Пока вангую на то, что у этого RwLockReadGuard есть своя реализация borrow (хотя смотрю в документацию и вроде как нет, Ctrl+клик ведёт в сильно общее место на пустую реализацию), которая не даёт мне вызвать собственный метод .borrow() от RefCell. Нашёл костыль этот с вызовом на другой строке метода от конкретной структуры (не нашёл варианта чтобы цепочку продолжить, может есть?), но такое себе. #rust
2 года назад
Приятно, что из-за того что тут enum'ы это другое, можно понятные значения писать, а не: generationId: string | null; collectionId: string | undefined; А потом в коде неочевидные штуки делать, мол, если generationId === null, то это значит что нужно с начала всё читать, а если collectionId не указана, подразумевается что это текущая коллекция. #rust
2 года назад
Ничего пока не одупляю, что за Box::pin() и откуда я взял BoxFuture (как я понял, не могу я просто так в HashMap сложить структурки, в которых будут лежать функции, которые возвращают Future, т.к. это слишком нечёткий тип, который будет требовать параметризации, а набор структур я не смогу параметризировать). И мьютексов уже каких-то пришлось наделать, но похоже что простенький эндпоинт который использует какое-то общее состояние сделать получилось, уже хорошо :) #rust
2 года назад
Ну что, попробуем #rust где-то в четвёртый раз, посмотрим, вспомню ли я как воевать с borrow checker'ом. Решил всё-таки не писать промежуточную версию на ноде, которая будет уже не в памяти работать, а в каком-нибудь leveldb/rocksdb все данные хранить. Попробуем рискнуть сразу по-хардкору пойти. Я вроде как понимаю, какие куски мне нужно дописать, основные алгоритмы у меня есть, думаю что должно будет взлететь. Начну с простого — выясню как тут делать HTTP-интерфейсы, как работать с RocksDB, как тут устраивают свои event loop'ы для асинхронщины. Ну а там уже начнём реализацию методов портировать с ноды (хотя местами оно усложнится тем что данные не в сплошном массиве, но может просто обёртку какую придумаю чтобы выглядело почти так же). #diffbelt
2 года назад
График выглядит стрёмно, плюс я где-то что-то сломал, что база ломается после дампа и восстановления (надеюсь завтра подебажить), но похоже что циферки начинают считаться. Завтра проверю на реальных данных, точно ли посчитало правильно (на глаз пока выглядит валидно) и тестов допишу на всякий случай. Удобно, кстати, тесты писать. Взял себе генератор псевдослучайных чисел, зафиксировал ему зерно, нагенерировал тыщ десять значений разных, положил их в Map<string, string>, записал их в базу, провёл махинации базой, провёл те же махиниции над Map'ом, сравнил. Потом произвёл «случайных» изменений, опять сравнил. #diffbelt
2 года назад