CodeFest 2013. Русанов П. — Есть ли жизнь в оффлайне? Кеш,...

Post on 14-Nov-2014

618 views 0 download

description

http://2013.codefest.ru/doklad/60

Transcript of CodeFest 2013. Русанов П. — Есть ли жизнь в оффлайне? Кеш,...

Есть ли жизнь в оффлайне?Кеш, транзакционный лог и проблемы синхронизацииРусанов ПетрLinguaLeohttp://lingualeo.com

Зачем нам оффлайн если интернет есть везде?

В городах полно Wi-Fi спотов

На улице есть 3G

Интернет есть даже в транспорте

И в самых неожиданных местах

• На Северном полюсе

• На вершине горы Эверест

• В пустынях ОАЭ

• На МКС

Вроде бы все прекрасно, все счастливы и можно

не париться

К сожалению, реальность такова:• 3G доступен только в больших городах, покрытие не 100%

• 3G не везде безлимитный, порой очень дорогой

• В роуминге цены на 3G достигают безумных цифр

• Интернет в самолетах стоит примерно ~$20 за 10 Mб, средняя скорость ~0.05 Мбит/сек

• В метро связь доступна только на станциях и то не на всех

• Даже домашний интернет могут отключить, если забыл оплатить :)

История из жизни

Лечу в Новосибирск на CodeFest

Лететь долго, занятьсянечем

Выучу пожалуй пару фраз на испанском

WHUUUT??

Итого

• Пользователю все равно: находится он в онлайне или нет, доступна сейчас сеть или пропала

• Пользователи расстраиваются если приложение перестает функционировать в отсутствии интернета

• При дальних перелетах/поездках/в метро у людей полно свободного времени, но нет интернета — это НУЖНО использовать, особенно в образовательных приложениях, где львиную долю функций можно реализовать без необходиомости постоянного доступа к сети

Есть решение!

• Кеширование медиа-файлов

• Хранение всех пользовательских данных в локальной БД

• Сохранение действий пользователя, произведенных в оффлайне

• Синхронизация с сервером при появлении доступа к сети

Кеширование

Кеши бывают разные

Требования

1. Возможность хранения данных не только в памяти, но и на диске

2. Поддержка политик (изменяемое поведение для разных типов запросов)

3. Строгое следование заголовкам HTTP, обозначающим правила кеширования и перевалидации (Cache-Control, Expires, Last-Modified)

4. Возможность отправки conditional GET

HTTP code: 200Age: 0Via: 1.1 varnishX-Varnish: 1103200550Accept-Ranges: bytes, bytesLast-Modified: Tue, 11 Sep 2012 14:01:07 GMTContent-Type: audio/mpegConnection: keep-aliveExpires: Tue, 26 Mar 2013 18:32:41 GMTServer: nginx/1.2.7Content-Length: 4752Date: Tue, 19 Mar 2013 18:32:41 GMTCache-Control: max-age=604800

Что кешировать?

• НЕ нужно кешировать всё - долго и занимает много места на диске

• Данные, без которых оффлайн работа будет невозможна

• Опциональные наборы данных лучше кешировать по запросу пользователя

Политики кеширования

•Политика определяет поведение при наличии закешированных данных и при ошибке

•Оффлайн режим для разработчика по сути представляет собой выполнение всех запросов с ошибкой

•Для запросов данных, которые предполагается кешировать для оффлайн работы, важно указать именно политику при ошибке (напр. в ASIHTTPRequest для iOS это ASIFallbackToCacheIfLoadFailsCachePolicy)

•В оффлайне лучше показать устаревшие данные, чем ошибку

Итого

• Выбираем правильную библиотеку для кеширования, соответствующую описанным требованиям

• Выставляем правильные политики для кешируемых запросов

• Реализуем выборочное кеширование и по запросу пользователя

Сохраняем историю действий пользователя

Я знаю, что вы сделали прошлым летом

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

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

Не нужно изобретать велосипедМожно использовать принцип, аналогичный триггерам в транзакционных SQL или хукам в VCS (post commit):

Пользователь совершил действие

Изменился объект в БД

Произошел commit в базу

Сработал post-commit

hook

Изменение сохранилось в лог

• Доступ к данным осуществляется исключительно через Data Access Objects (DAO), полностью инкапсулирующих в себе работу с БД

• Через proxy-классы перехватываются методы DAO для добавления/удаления/изменения объектов и устанавливаются transaction listener-ы в качестве post-commit хуков (триггеров)

• Сразу после коммита в БД вызываются transaction listener-ы и сохраняют изменения в БД в виде записей транзакционного лога, содержащие тип операции (добавление/удаление/изменение), таймштамп, сам измененный объект и вспомогательные данные

• Взаимоисключающие и дополняющие операции объединяются для компактности данных

Реализация: как это сделано в LinguaLeo

Реализация: DAO и вложенные транзакцииВ каждом объекте DAO имеется экземпляр объекта транзакции

В основе объекта транзакции лежит счетчик открытых и закрытых транзакций (аналогично счетчику ссылок retainCount у NSObject в Objective-C)

При выполнении [transaction begin] счетчик увеличивается

При выполнении [transaction commit] счетчик уменьшается

Как только счетчик стал равен нулю, выполняется реальный commit в базу данных и сразу после выполняется метод didCommitTransaction у всех transaction listener-ов

Пример: transaction listener и DAO@implementation LLLoggingOfAddingBonusTransactionListener

- (void)didCommittedTransaction { LLTransactionLogRecord *record = [LLTransactionLogRecord bonusLog]; record.operation = @(LLOperationTypeInsert); record.loggedEntity = self.bonus; [self.transactionLogDAO insertRecord:record];}

@end

@protocol LLQuestDAO <LLDAO>

- (void)deleteAllBonuses;- (NSArray *)bonusesForType:(LLQuestType)type;- (void)insertBonus:(LLBonus *)bonus;- (void)udpateBonus:(LLBonus *)bonus;

- (LLQuest *)currentQuestStateForType:(LLQuestType)type;- (void)updateCurrentQuestState:(LLQuest *)questState forType:(LLQuestType)type;

@end

Пример: proxy-класс для DAO@implementation LLAOPLoggingDAOProxy

+ (id)newProxyForWordDAO:(id <LLWordDAO>)wordDAO { id result = [[[LLAOPLoggingDAOProxy alloc] initWithInstance:wordDAO] autorelease]; [result interceptMethodEndForSelector:@selector(insertWordsToUserDictionary:) interceptorSelector:@selector(logAddingNewWords:)]; [result interceptMethodEndForSelector:@selector(insertWordsFromGlossaryToUserDictionary:) interceptorSelector:@selector(logAddingNewWordFromSimpleWords:)]; [result interceptMethodStartForSelector:@selector(updateWord:) interceptorSelector:@selector(logUpdatingWord:)]; [result interceptMethodStartForSelector:@selector(updateWords:) interceptorSelector:@selector(logUpdatingWords:)]; [result interceptMethodStartForSelector:@selector(deleteWordsFromUserDictionary:) interceptorSelector:@selector(logDeletingWords:)]; [result interceptMethodStartForSelector:@selector(updateSimpleWords:asKnown:) interceptorSelector:@selector(logUpdatingSimpleWord:)];

return result;}

@end

Пример: транзакционный логВ итоге лог выглядит примерно так:[

{

'data': { ... },//поля объекта класса LLBonus

'entity_type':1,//тип сущности (LLBonus)

'operation':0,//тип операции (0 - вставка, 1 - обновление, 2 - удаление)

'timestamp':'2013-02-20T10:33:46+0200'//таймштамп

},

{

'data': { ... },

'entity_type':0,//LLWord

'operation':1,//обновить

'timestamp':'2013-02-21T11:25:40+0200'

}

...

]

Итого

• Всю работу с БД осуществляем в DAO

• Сохраняем все действия пользователя в лог не смотря на наличие/отсутствие сети

• Стараемся сделать лог компактным, объединяя записи

Синхронизация

Да будет сеть!Как только появляется доступ к сети, нужно синхронизовать данные с сервером:

• отправить на сервер сохраненные операции

• принять операции от сервера, совершенные на сайте и других устройствах

• данных могло накопиться много - нужно упаковать данные, чтобы синхронизация прошла быстро и не "съела" много траффика

Проблемы синхронизации У пользователя может быть несколько устройств

Какие-то из них могут находиться в оффлайне

В них будет накапливаться история операций на основе устаревших данных

При выходе этих устройств в сеть могут возникать конфликты

Ничего не напоминает?

Принцип VCS для синхронизации

Это самый обычный кейс для систем контроля версий вроде SVN или Git

Все решается вводом численного обозначения ревизии данных (или хеша), которое контролирует сервер

При синхронизации:

Если на устройстве устаревшая версия данных, сначала накатываются и мержатся изменения с сервера (svn up/git pull) и только затем делается аплоад накопившихся операций на сервер (svn commit/git push)

Синхронизация: схема VCS

rev 0

register

rev 0

login

rev 0

добавить ‘get up’ с переводом ‘вставать’

добавить ‘get up’ с переводом ‘дорожать’

syncpush

rev 1

syncpush

client revison is old!

syncpull

MERGEwithrev 1

syncpush

rev 2

syncpull

rev 2

обновить ‘get up’: перевод ‘дорожать’

rev 2

Итого

• Регулярно делаем синхронизацию, чтобы минимизировать объем отправляемых данных за раз

• Применяем методы синхронизации из систем контроля версий

• При неудачной синхронизации делаем rollback

Русанов Петр,LinguaLeo

http://lingualeo.com

prusanov@lingualeo.com+7 (920) 0269579

я

Вопросы?