Cache Pattern for Offline Web Applications

33

Transcript of Cache Pattern for Offline Web Applications

Page 1: Cache Pattern for Offline Web Applications
Page 2: Cache Pattern for Offline Web Applications

Cache Pattern for Offline Web ApplicationsRobert KroegerJune 27, 2009

Post your questions for this talk on Google Moderator:code.google.com/events/io/questions

Page 3: Cache Pattern for Offline Web Applications

3

Why Mobile Web Applications?

• Pro: server-side features that cannot be accommodated in a mobile device– Mobile devices have limited storage– Mobile devices have limited CPU

• Pro: ease of distribution– No app store approval process– Launch on your schedule– Update frequently

• But: use of wireless connections can make this much too slow

Page 4: Cache Pattern for Offline Web Applications

4

The Connection Problem

Internet ServerEDGE/3G

High latency

Flaky Connection

Page 5: Cache Pattern for Offline Web Applications

Mobile Device

Internet Server

Internet ServerDevice-local Cache

Web application

UI

Mobile Device

Web application

UI

5

Caching Keeps Mobile Web Apps Responsive

Update UIUpdate UI

Modify CachePush To Server

Update Cache

No Synchronous Connection From Web UI to Server

Page 6: Cache Pattern for Offline Web Applications

CacheCached

Content to UI

Changes from

UI

Updates bound

for Server

Changes from

server

6

Implementing Cache Pattern

Browser

platformDB

Storing the data

Page 7: Cache Pattern for Offline Web Applications

7

Cache Storage: Structured Storage

• Structured storage capability in HTML5 and Gears– A SQL database– Local to the client– Can access in absence of network connection

• Keep the cached contents here– Persistent across browser restarts– ACID properties maintain consistent database state

Page 8: Cache Pattern for Offline Web Applications

JavaScript

Thread

database action

JavaScript

Thread

SQL

Execution

Thread

Gears

HTML5

database action

8

The Asynchronous Programming ModelStructured Storage: Gears != HTML5

Function call

Function call

Return

Function call (back)

Other JavaScript

Page 9: Cache Pattern for Offline Web Applications

9

SQL database

Synchronous Programming

model

Asynchronous programming

modelAndroid Platform iPhone

HTML5 Structured Storage

Google Gears Database

Portability?Gears vs HTML5 Structured Storage

Page 10: Cache Pattern for Offline Web Applications

Common API

HTML5

Structured

Storage

ResultsStatement Transactions

Google Gears

Asynchronous worker

10

Web Storage Portability Layer (WSPL)

• Code to one common API

• Asynchronous programming model

• Gears worker implements the asynchronous programming model above synchronous database

• Available at code.google.com/webstorageportabilitylayer

One API For Gears and HTML5

Page 11: Cache Pattern for Offline Web Applications

CacheCached

Content to UI

Changes from

UI

Updates bound

for Server

Changes from

server

Browser

platformDB

WSPL API

11

Build Portable Cache above WSPL

Asynchronous interface

Page 12: Cache Pattern for Offline Web Applications

Cache

Request Cached

Content: getValues

Acknowledge UI

Change: ackCallback

Updates bound for

Server: sendXhrPost

Server response:

ackChange

WSPL API

execute callback

Server response:

insertUpdate

Cached Content to

UI: valueCallback

Changes from UI Action:

applyUIChange

Get

Apply

Changes

Server

Merge

Ack

Changes

Queries bound for

Server: sendXhrPost

12

Asynchronous Cache Architecture

Page 13: Cache Pattern for Offline Web Applications

13

SimpleNotes

• Toy application– keep notes– server side left as

audience exercise

• Demonstrates use of the WSPL library

• Included with WSPL • Three different “pages” in

the application

A Concrete Example of the Cache Pattern

List Notes Page View Note

Time SubjectSubject

Time Subject

...

Note content

Click

New

Back

Edit Note

Subject

Note content

Save Edit

Page 14: Cache Pattern for Offline Web Applications

14

Key Cache Design Aspects• Hit Determination: what data is cached?

– A contiguous range of notes

• Refresh: when to update the cached data with server changes?– Every request for a list of notes

• Eviction: what to discard to make room for new data• Coherency: how to merge server changes into cache

– Always send updates ahead of queries– Reapply actions for unacknowledged updates

Page 15: Cache Pattern for Offline Web Applications

google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_ = new google.wspl.Statement( 'CREATE TABLE IF NOT EXISTS cached_notes (' + 'noteKey INTEGER UNIQUE PRIMARY KEY,' + 'subject TEXT,' + 'body TEXT' + ');');

google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_ = new google.wspl.Statement( 'CREATE TABLE IF NOT EXISTS write_buffer (' + 'sequence INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT,' + 'noteKey INTEGER,' + 'status INTEGER,' + 'subject TEXT,' + 'body TEXT' + ');');

google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_ = new google.wspl.Statement( 'SELECT MIN(noteKey) as minNoteKey FROM cached_notes;');google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_ = new google.wspl.Statement( 'SELECT MAX(noteKey) as maxNoteKey FROM cached_notes;');

15

Simple Notes Tables

Separate write buffer: actions vs state

Page 16: Cache Pattern for Offline Web Applications

google.wspl.simplenotes.Cache.prototype.startCache = function(callback) { var statc = 0; var self = this;

var perStatCallback = function(tx, result) { google.logger('perStatCallback'); if (statc == 4) { self.start_ = (result.isValidRow()) ? result.getRow().minNoteKey : -1; self.serverStart_ = self.start_; // Temporary. Remove when server exists. } else if (statc == 5) { self.end_ = (result.isValidRow()) ? result.getRow().maxNoteKey : -1; self.serverEnd_ = self.end_; // Temporary. Remove when server exists. } statc++; };

this.dbms_.executeAll([ google.wspl.simplenotes.Cache.CREATE_CACHED_NOTES_, google.wspl.simplenotes.Cache.CREATE_WRITE_BUFFER_, google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_, google.wspl.simplenotes.Cache.CREATE_REPLAY_TRIGGER_, google.wspl.simplenotes.Cache.DETERMINE_MIN_KEY_, google.wspl.simplenotes.Cache.DETERMINE_MAX_KEY_], {onSuccess: perStatCallback, onFailure: this.logError_}, {onSuccess: callback, onFailure: this.logError_}); google.logger('finished startCache');};

16

Simple Notes Creation

Asynchronous here too

Page 17: Cache Pattern for Offline Web Applications

Cache

Acknowledge UI

Change: ackCallback

Updates bound for

Server: sendXhrPost

Server response:

ackChange

execute callback

Server response:

insertUpdate

Changes from UI Action:

applyUIChange

Apply

Changes

Server

Merge

Ack

Changes

Queries bound for

Server: sendXhrPost

WSPL API

Get

Request Cached

Content: getValues

Cached Content to

UI: valueCallback

createTransaction

execute

inTransactionGetNotes

accumulateResults

17

getValues Hit Call Flow

Page 18: Cache Pattern for Offline Web Applications

18

getValues Example Code (1)

google.wspl.simplenotes.Cache.prototype.getValues = function(type, query, valuesCallback) {

// Reduce any query to what would be available from the server query[0] = Math.max(this.serverStart_, query[0]); query[1] = Math.min(this.serverEnd_, query[1]);

if (type == 'list') { this.getNoteList_(query[0], query[1], valuesCallback); } else if (type == 'fullnote') { this.getOneNote_(query[0], valuesCallback); }};

Same idea as getNoteList_

Page 19: Cache Pattern for Offline Web Applications

19

getValues Example Code (2)google.wspl.simplenotes.Cache.prototype.getNoteList_ = function(start, end, valuesCallback) { var notes = [];

var accumulateResults = function(tx, result) { for(; result.isValidRow(); result.next()) { notes.push(result.getRow()); } };

var inTransactionGetNotes = function(tx) { tx.execute(google.wspl.simplenotes.Cache.LIST_CACHED_NOTES_. createStatement([start, end]), { onSuccess: accumulateResults, onFailure: this.logError_}); };

var hit = this.isCacheHit_(start, end); this.dbms_.createTransaction(inTransactionGetNotes, {onSuccess: function() { valuesCallback(notes, hit); }, onFailure: this.logError_});

if (hit) { this.fetchFromServer(this.start_, this.end_); // Refresh } else { this.fetchFromServer(Math.min(this.start_, start), Math.max(this.end_, end)); this.lastMiss_ = {callback: valuesCallback, start: start, end: end}; }}; In transaction

accumulateResultsgetNoteList inTransactionGetNotes valuesCallback

Need a cache membership scheme

Page 20: Cache Pattern for Offline Web Applications

20

Hitting and Missing

• isCacheHit– Application Specific– Simple approach here: cache contains a contiguous range of

notes.

– Cache maintains the start and end values in JavaScript– Cache grows its range by making a server request for the missing

values.

• Avoid database accesses

google.wspl.simplenotes.Cache.prototype.isCacheHit_ = function(start, end) { return start >= this.start_ && end <= this.end_;};

Page 21: Cache Pattern for Offline Web Applications

Server response:

ackChange

execute callback

Server response:

insertUpdateServer

Merge

Ack

Changes

Queries bound for

Server: sendXhrPost

Get

Request Cached

Content: getValues

Cached Content to

UI: valueCallback

Updates bound for

Server: sendXhrPost

Cache

Acknowledge UI

Change: ackCallback

Changes from UI Action:

applyUIChange

WSPL API

Apply

Changes

executeAll ackCallBack

updateTrigger

21

applyUIChange Call Flow

Update via a trigger

Page 22: Cache Pattern for Offline Web Applications

22

applyUIChange Code

google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_ = new google.wpsl.Statement( 'INSERT INTO write_buffer (' + 'noteKey, status, subject, body )' + 'VALUES(?,?,?,?);');

google.wspl.simplenotes.Cache.prototype.applyUiChange = function(noteKey, subject, body, ackCallback) { var self = this; var update = [noteKey, 2 | 8, subject, body]; var stat = google.wspl.simplenotes.Cache.INSERT_UI_UPDATE_.createStatement( update);

this.dbms_.execute(stat, null, {onSuccess: function() { ackCallback(noteKey); }, onFailure: function (error) { self.logError_(error); ackCallback(-1); }});};

Page 23: Cache Pattern for Offline Web Applications

23

Why Triggers: Avoid Ping-Ponging

• Read-modifiy-write operations are inefficient• A trigger runs entirely inside SQL thread

Long Transactions Are Bad

In transaction

txModifytxGet end of txonclick

JavaScript

Thread

SQL

Execution

Thread

select update

In transaction

txModify end of txonclick

JavaScript

Thread

SQL

Execution

Thread

trigger update

Page 24: Cache Pattern for Offline Web Applications

24

applyUIChange Trigger Code

google.wspl.simplenotes.Cache.CREATE_UPDATE_TRIGGER_ = new google.wspl.Statement( 'CREATE TRIGGER IF NOT EXISTS updateTrigger ' + 'AFTER INSERT ON write_buffer ' + 'BEGIN ' + ' REPLACE INTO cached_notes ' + ' SELECT noteKey, subject, body ' + ' FROM write_buffer WHERE status & 8 = 8; ' + ' UPDATE write_buffer SET status = status & ~ 8; ' + 'END;' );

Update cache from write_buffer

• status: a bit-field of states– 1 The update is inflight to the server– 2 The update needs to be (re)sent to the server– 8 The update needs to replayed against the cache

Trigger is only way to execute several statements

Page 25: Cache Pattern for Offline Web Applications

Cache

Server response:

ackChange

executecallback

Ack

Changes

Queries bound for

Server: sendXhrPost

Get

Request Cached

Content: getValues

Updates bound for

Server: sendXhrPost

Acknowledge UI

Change: ackCallback

Changes from UI Action:

applyUIChange

Apply

Changes

Server response:

insertUpdateServer

Merge

WSPL API

createTransaction

inTrans

executeAll

replayTrigger

afterInsert

Cached Content to

UI: valueCallback

25

insertUpdate Call Flow

Coherency: replay actions

Callback if servicing a cache miss

Page 26: Cache Pattern for Offline Web Applications

26

insertUpdate Codegoogle.wspl.simplenotes.Cache.prototype.insertUpdate = function(notes) { var self = this; var stats = []; var start = notes[0].noteKey; var end = notes[0].noteKey;

for (var i = 0; i < notes.length; i++) { stats.push(google.wspl.simplenotes.Cache.INSERT_NOTE_. createStatement([notes[i].noteKey, notes[i].subject, notes[i].body])); start = Math.min(start, notes[0].noteKey); end = Math.max(end, notes[0].noteKey); } stats.push(google.wspl.simplenotes.Cache.EVICT_.createStatement([start, end])); stats.push(google.wspl.simplenotes.Cache.FORCE_REPLAY_);

var inTrans = function(tx) { self.start_ = start; self.end_ = end; tx.executeAll(stats); };

var afterInsert = function(tx) { if (this.lastMiss_ && this.isCacheHit_(this.lastMiss_.start, this.lastMiss_.end)) { this.lastMiss_.callback(notes); this.lastMiss_ = undefined; } }; this.dbms_.createTransaction(inTrans, {onSuccess: afterInsert, onError: this.logError_});};

Must update here to align JS change with transaction boundary

Callback if this update from the server satisfies a pending request

Page 27: Cache Pattern for Offline Web Applications

Cache

Acknowledge UI

Change: ackCallback

Updates bound for

Server: sendXhrPost

Server response:

ackChange

execute callback

Server response:

insertUpdate

Changes from UI Action:

applyUIChange

Apply

Changes

Server

Merge

Ack

Changes

Cached Content to

UI: valueCallback

Queries bound for

Server: sendXhrPost

WSPL API

Get

Request Cache-miss

Content: getValues

executeAll

ackAndPost

accumulateUpdates

27

fetchFromServer Call FlowHandling Cache Misses and Refresh

Page 28: Cache Pattern for Offline Web Applications

28

fetchFromServer Codegoogle.wspl.simplenotes.Cache.prototype.fetchFromServer = function(start, end) { var now = this.dbms_.getCurrentTime(); if (start >= this.start_ && end <= this.end_ && now - this.lastRefresh_ < google.wspl.simplenotes.Cache.TIME_BETWEEN_REFRESH_) { return; }

var updates = []; var self = this; var flag = 1; var sql = []; sql.push(google.wspl.simplenotes.Cache.GET_UPDATES_TO_RESEND_); sql.push(google.wspl.simplenotes.Cache.MARK_AS_INFLIGHT_);

var accumulateUpdates = function(tx, rs) { if (flag == 1) { for(; rs.isValidRow(); rs.next()) { updates.push(['u', rs.getRow()]); } flag++; } };

var ackAndPost = function() { updates.push(['q', {start: start, end: end}]); self.sendXhrPost(updates); };

this.dbms_.executeAll(sql, {onSuccess: accumulateUpdates, onFailure: this.logError_}, {onSuccess: ackAndPost, onFailure: this.logError_});};

Page 29: Cache Pattern for Offline Web Applications

In Flight

Write

Buffer

Time

1

1 321

1

Q

2

1

2

2

1sendXhrPost

fetchFromServer applyUIChange

fetchFromServer

3

1ackChange

!

ackChange

Q

applyUIChange

insertUpdate

29

Maintaining Server ConsistencyResend failures before queries, then reapply actions

Page 30: Cache Pattern for Offline Web Applications

30

Miscellaneous Tidbits

• Messages to server– JSON-encoded– via XHR POST

• See the WSPL open source distribution for the remaining missing bits

• Asynchronous model decouples the execution order from the display order

• Triggers crucial to rapid development• SimpleNotes glosses over some hard problems:

– Database too slow to serve as application model– Too much data traffic

Page 31: Cache Pattern for Offline Web Applications

31

Summary

• Cache pattern workable in real products: Mobile Gmail for iPhone and Android.

• WSDL library same• Workable local storage solution across iPhone and Android

Page 32: Cache Pattern for Offline Web Applications

Post your questions for this talk on Google Moderator:code.google.com/events/io/questions

Q & A

Page 33: Cache Pattern for Offline Web Applications