(モバイル)アプリとサーバ間の同期方法 – Evernote way

アプリとサーバ同期の方法は、自力で生み出すのは試行錯誤するのは目に見えているので既存の方法を調べてみました。あまり確立した方法がない中、Evernoteが同期のスペック(EDAM synchronization)を公開していることを知り、そのスペックを読んでみました。

簡単に言うと、サーバ側のデータを中心として、複数クライアントからでも同期できるという方法です。クライアント側でがんばる方法とも言えます。

同期を構成する要素

  • Full SyncとIncremental Syncの2パターンがある。
  • 同期するオブジェクトはシークエンス番号(Update Squence Number: USN)をもつ。Create/Update/Deleteの度にインクリメントしてアサインする。
  • 同期するオブジェクトはGUIDという一意のIDをもつ。
  • サーバ側の変数は、
    • updateCount: 最大USN
    • fullSyncBefore: 削除済みリストをクライアントキャッシュから削除したいときに使うか。または、問題があった場合に、Force FullSyncさせるためのもの。時間型変数(DateTime)。
  • クライアント側の変数は、
    • lastUpdateCount: 最後に同期したときのサーバから得たupdateCount
    • lastSyncTime: 最後に同期した時間型変数(DateTime)
  • 各同期オブジェクトにdirtyフラグをもつ。変更があった場合に、同期対象にするためのフラグ。

では、具体例(TodoList)を用いて説明します。ユーザ毎にUpdateCountを持つので上記の変数は下記のように変化します。

  1. アカウント作成時に初期値user.updateCount = 0 となる
  2. TodoListのタスクが作られると、task1.usn=1, user.updateCount=1
  3. もう一つタスクが作られると、task2.usn=2, user.UpdateCount=2
  4. task1をアップデートして、task1.usn=3, user.updateCount=3
  5. task2を削除してhard deleteでuser.updateCount=4、soft Deleteはtask1.usn=4, user.updateCount=4
  6. Taskの上位概念のProjectを作成project1.usn=5, user.updateCount=5

同期するオブジェクトに番号がふられるので、クライアント側でどの時点のupdateCount(=lastUpdateCount)を記録しておけば、インクリメントで該当オブジェクトを取得できる。

クライアントとサーバ間の同期方法

それでは、同期方法をみていきます。クライアント側からの見方になります。

  1. 1度も同期した事がなければ、Full Sync。
  2. fullSyncBefore > lastSyncTime, の時、 Full Sync。(例えば、なんらかの理由でForceアップデートしたい時にfullSyncBeforeをその変更時間にすれば、強制的にFull Syncさせることができる)
  3. updateCount = lastUpdateCountの時、サーバ側に変更がないので、クライアント側の変更を更新する(=Send Changes)
  4. それ以外のときは、Incremental Syncする。

Full Sync

Full Syncとは全データに対して走査し、同期すること。クライアントのデータを全消しして、全部取得することではないので注意。スペック内のExampleが名前が一意である必要のあるTagを使って説明しているので少しわかりづらい。一意である必要がある場合は、Renameを考慮する必要がある。下記は、Todoのタスクのように一意性を問わない場合の例。

  1. 更新すべきデータを取得する。
  2. IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
  3. IDがクライアントにあって、サーバ側にない場合、
  4. クライアントのオブジェクトのdirtyフラグがない場合、そのデータをクライアントから消す
  5. dirtyフラグがある場合は、あとでアップロードする。
  6. IDがクライアントとサーバ両方にある場合は、オブジェクトのUSNを比較して、
  7. 同じなら、データは同期している
  8. 同じで、クライアント側にdirtyフラグがある場合は、後でアップロードする
  9. サーバ側のUSNが大きくて、dirtyフラグがない場合、サーバ側のデータに置き換える
  10. サーバ側のUSNが大きくて、dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
  11. サーバのデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
  12. 変更を更新する(=send Changes)

Incremental Sync

Full Syncと似ているが差分だけとってくるので、各オブジェクトのUSNの比較を行わなくて良い。削除済みリストとクライアントのリストを比較して必要なら削除する必要がある。

  1. クライアントのlastUpadteCount以降のデータを取得する
  2. IDのリストを作って、クライアントDBと比較し、クライアント側にIDがない場合は、クライアントDBに追加する
  3. IDがクライアントとサーバ両方にある場合は、
  4. dirtyフラグがない場合、サーバ側のデータに置き換える
  5. dirtyフラグがある場合、コンフリクト発生しているのでマージするか、ユーザにコンフリクト状態をレポートする
  6. サーバからのデータがから削除済みのリストを作成し、対象データがある場合はクライアント側から削除
  7. サーバ側のデータマージが終わったら、サーバのupdateCountをクライアントのlastUpadteCountにして、現在の時間をlastSyncTimeにする
  8. 変更を更新する(=send Changes)

Send Changes

  1. ローカルにあるdirtyフラグのあるオブジェクトに対して、
  2. 新しいオブジェクト(USNが設定されてない)場合は、サーバ側にCreateをリクエストする。
  3. オブジェクトが変更された(USNが設定されている)場合は、サーバ側にUpdateをリクエストする。
  4. レスポンスとして、
    • USN = lastUpdateCount + 1のときは同期状態。クライアントのlastUpdateCountを更新する
    • USN > lastUpdateCount + 1のときは、同期していないので、後でIncremental Syncをする。

BOSE Quietcomfort35レビュー

今働いているスタートアップの会社の人数が少しずつ増えてきて10人程度になってきました。オフィスは一つの部屋なのでミーティングやディスカッションが行われていれば、自然と情報が入ってきてだいたい今のビジネスの方向性や問題点などがわかってきます。

ここで問題となってきたのが、プログラムを書くときに集中してコーディングしないといけない、しかし、室内のノイズで集中できないケースです。集中が必要なのが、深いロジックを考える必要があるときです。浅いロジックの思考中は、ノイズがあっても平気ですし、インタラプトされても思考地点に復旧が容易です。

参考: Never interrupt a programmer Never interrupt a programmer

そういった背景でノイズキャンセリングのイヤホンまたはヘッドホンを探しだしました。

最初に迷ったのがイヤホンかヘッドホン。イヤホンは携帯性にすぐれていて、耳に密着している分、ノイズキャンセルが強い。ヘッドホンは携帯性はないけど耳全体を覆うので音のクオリティが良いらしい。今回、私はこのような性能差よりも、コーディング中に”俺は集中してるから声をかけないでね”オーラを出せるヘッドホンタイプを探すことにしました。

ノイズキャンセリングは少し高くて、その中でも安価なものから高価なものまで沢山あります。自分の半生を振り返ると、例えば、その昔、安価なCreativeのmp3プレイヤーを持ってましたが、中途半端だったため結局使わなくなり、高価だったiPodに移行してから使い出したことがあります。中途半端なブランドを買うとダメなようです:)

なので、ブランド力のあるBOSEを選ぶことにしました。これは、ブランドだけでなく同僚がもってたBOSEのイヤホンタイプのQuietComfort20を使わせてもらったら、すごくよかったからです。

そして、少し調べてみるとオフィスの近くにBOSEのショールームがあることを発見。お昼休みに行ってきました。ショールームには、QuietComfortのいろいろなシリーズがありました。飛行機のフライト音を再現してくれて、ノイズキャンセリングの効果を全てのシリーズで試させてもらいました。気づいたらQuietComfort35を買ってました(笑)。2週間ずっと迷ってたのに一撃ですね。恐るべし、ショールーム。きっと家を買う人もショールームマジックにやられるんだろうな。

QuietComfort35にした決め手は、Bluetooth。そして、発売して2週間しか経ってないことです。家電は新しいのを買わないと買った喜びが半減してしまう(笑)

使ってみて感想は、下記のような感じ。一言で言うと、買って正解でした。

  • ノイズキャンセリングは秀逸。(人の声が全く聞こえなくなるわけではないがそれはそういうものらしい)
  • ノイズキャンセリングを使うとノイズ除去のための音量を上げなくて良いので、耳に優しい。
  • iPod以来の家電を持ってて嬉しい感
  • なんだか眠くなる。
  • 歩いてる時に使用すると車が接近してるのがわからなくて、危険。
  • ノイズってこんなに世界にあふれてるんだと感じる
  • 集中してコーディングできる!

ビジネス面では、メリットもあるけどデメリットもあるなって思ってきてます。コーティングに集中できるけど、その反面、オフィス内で行われてるディスカッションを聞いてたり、介入したりすることができなくなって、コミュニケーションの妨げになる一面がある。

本当に集中したいときだけ使うように心がけようって思ってます。

そういった意味ではオフィスが手狭なことは良いこともあるかもしれない。部屋が増えると、コミュニケーションが阻害されそう。別の仕組みが必要になってくるだろうな。

フレッツ光でプロバイダを乗り換えたら安くなって下りの速度が6倍になった話

背景

家でFacebookを眺めてると動画フィードでローディングのアイコンが頻繁に出て、ネットが遅いって感じてきました。特に夜に遅くなってる気がしてました。

調査

ネットでインターネットの速度を測ってみると3Mbit/sでした。光なのに4G回線より遅い。。すぐさまネットで調べてみると、ひかりではADSLの基地局の家からの距離問題は特にないけど、プロバイダの混雑具合で速度が変わるケースが出てくるらしい。

事実確認

契約していたプロバイダはbiglobeさん。3年目で初期キャンペーンの旨味はもうなく、切り替えに特に違約金も発生しない。(プロバイダは運らしく。biglobeが悪いわけではなく、私の地区では厳しいかったということです)。

Now is the time to switch my provider!

行動1

乗り換えでぐぐってみるとkakaku.comのアドがトップに。何も考えず、クリック。一番安くてBiglobeじゃないYahooBBに申し込みました。指定した時間で電話がかかってきて、説明を受けました。その内容が、

  • キャンペーンのキャッシュバックは数ヶ月後に発生
  • キャンペーン適用には2年縛り契約
  • 2年縛り契約には更新月があり、それを逃すと自動更新。
  • 自動更新月以外は違約金発生
  • なんたらっていうオプションに自動参加(あとから取れると言う説明)
  • 必要あるなしに関わらず、ルーターみたいのが送られてくる(それも初月無料だけど解約しないと課金発生)

ここで、電話をインタラプトして、「すいません、かなり面倒なので申し込むの止めます」って言いました。。 もうキャンペーンのキャッシュバック以上に労力ないしは気苦労がかかりそうです。。

電話を切って、再考。そもそもこういったキャンペーン打ってるとこに申し込みが集中して、プロバイダの回線がパンクしてるのではないかと。マイナーだけどいいプロバイダを探してみようと思い立ちました。

行動2

そういえば、実家のプロバイダは1998年ごろからずっとAsahiネット。調べてみると東証一部上場の企業なんですね。上場がすごいなーなんて思いつつ、HPで乗り換えキャンペーンを発見。早速申し込みました。(biglobeは解約)

設定

3日後にIDとパスワードが郵送で送られてきて、192.168.1.1を叩いて変更。すぐにつながりました。

結果

Terminalを開いてspeedtest-cliをダウンロード。

$ pip install speedtest-cli

日本検索。

$ speedtest --list | grep Japan

で、テスト。

# AsahiNet
$ speedtest --server 6766
Retrieving speedtest.net configuration...
Retrieving speedtest.net server list...
Testing from Asahi Net (110.4.199.153)...
Hosted by JAIST(ino-lab) (Nomi) [299.95 km]: 20.093 ms
Testing download speed........................................
Download: 30.47 Mbit/s
Testing upload speed..................................................
Upload: 20.10 Mbit/s

30Mbit/s !!! やっほい!

でプロバイダをbiglobeに戻してspeedtest.

# biglobe
$ speedtest --server 6766
Retrieving speedtest.net configuration...
Retrieving speedtest.net server list...
Testing from BIGLOBE (60.237.90.6)...
Hosted by JAIST(ino-lab) (Nomi) [286.39 km]: 106.427 ms
Testing download speed........................................
Download: 4.69 Mbit/s
Testing upload speed..................................................
Upload: 15.55 Mbit/s

4.69Mbit/s。 全然違う。

乗り換えてよかった! 月額も900円ちょいから700円になってうれしい。

ReduxでReducerのファイルを分割する方法

stateが多くなるとreducerのファイルが巨大化してきます。ファイルを分けた方が編集も楽になるし、コード全体の見通しもよくなると思います。

分割方法は簡単で、index.jsファイルを作り、各状態のファイルを読み込み、configureStore.jsでそのindex.jsを読み込みます。

ディレクトリツリー

app
├── Root.js
├── actions
│   └── user.js
├── components
├── configureStore.js  
├── containers
│   └── App.js
└── reducers
    ├── index.js # configureStore.jsで読み込む
    └── user.js # index.jsで読み込む

configureStore.js

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import reducer from './reducers/index'  // indexを呼ぶ

const loggerMiddleware = createLogger()

const createStoreWithMiddleware = applyMiddleware(
  thunkMiddleware,
  loggerMiddleware,
)(createStore)

export default function configureStore(initialState) {
  return createStoreWithMiddleware(reducer, initialState) //いつも通りにstore作成
}

index.js

import { combineReducers } from 'redux'
import user from './user'

export default combineReducers({
  user
})

user.js

import { SIGNUP, LOGIN, LOGOUT, LOGIN_COMPLETED, SIGNUP_COMPLETED,
  SIGNUP_FAILED, LOGIN_FAILED } from '../actions/user';

export default function user(state = {
  isSigningUp: false,
  isLoggingIn: false,
  loggedIn: false,
  signUpFailed: false,
  loginFailed: false,
  tasks: [],
  projects: {},
  error: null,
},  action) {
  switch (action.type) {
    case SIGNUP:
      return Object.assign({}, state, {
        isSigningUp: true,
      });
.....    

これで巨大reducerファイルを編集しなくて良くなる:)