http://layervault.tumblr.com/post/37923376877/revisiting-activity-feed
- Activity Feedは賢くないといけない
- Tackling Information Density
- アクティビティフィードは関連するイベントを1つのストーリーにまとめる
- アクティビティアイテムはMySQLに格納されている
- アイテム生成はwrite heavy
- Taming the DOM
- アクティビティフィードはページングされる
- アクティビティフィードデータのプリロードとサーバーの高速化により、無限スクロールを再現できる
- アクティビティフィードHTMLはRedisに格納されたデータから描画される
- アクティビティページを取得するAPIコールは、クライアントサイドのJavaScript処理を減らすためにHTMLを返す
- What is an Event?
- LayerVaultにおけるイベントの定義
- サイト上のすべてのイベントを定義するジェネリックな構造
- すべてのイベントはsubject, verb, 1つ以上のオブジェクト、いくつかのオプション属性を持つ
- subjectはイベントを行った人
- verbはsubjectが行ったアクション
- objectはアクションが行われたアイテム
- Creating an Event
- アクティビティイベントはLayerVaultにおけるいくつものアイテムについて作られる
- ファイル、フォルダ、ユーザーなど
- これらのアイテムはすでにモデルとして表現されているので、アクティビティフィードの機能を追加するのは容易
- 多重継承をサポートする言語であれば、これらのモデルをすべてアクティビティアイテムクラスから継承すればよいが、RubyではMixinを使う
- スコープとフィルタ
- スコープはアクティビティアイテムを誰が閲覧できるかを定義する
- フィルタはアクティビティアイテムがどのオブジェクトにアタッチされるかを定義する
- 生成されるアクティビティアイテムの数はスコープとフィルタの積による
- アイテムを各ユーザー、各フィルタに対して生成するため scope.each do |owner| filters.each do |filter|
- LayerVaultのすべてのファイル、フォルダ、プロジェクトがアクティビティフィードを持つということなので、heavy writes and fast readsになる
- アクティビティイベントはLayerVaultにおけるいくつものアイテムについて作られる
- Single-Table Inheritance
- 各アクティビティアイテムの責務を分離するためにSTIを使う
- 単純なアクティビティタイプ (Ryan created Test.psd)
- 複雑なアクティビティタイプ (Ryan delivered Test.psd to Basecamp, Campfire and Dropbox)
- STIによって異なるシチュエーションの処理を分離できる
- すべてのフィードアイテムタイプは固有のクラスを持ち、ActivityFeedItemクラスを継承する
- 各アクティビティアイテムの責務を分離するためにSTIを使う
- Processing the Feed
- アクティビティフィードの処理はバッチ処理である
- すべてのアクティビティアイテムについて処理を繰り返す
- 現在のアイテムが最後のアイテムと関連するものであれば、それらを結合する
- そうでなければ最後のバッチを処理に送り出して、新しいバッチを生成する
- 判定ルールは、同じアクションで同じオブジェクトタイプかどうか current_batch = [] feed_items.each do |feed_item| unless feed_item.should_batch(last_item) batches.push current_batch current_batch = [] end last_item = feed_item current_batch.push feed_item end
- Aggregation
- すべてのアイテムがバッチに集められたら集約を行う
- Aggregatorではバッチデータの処理と最終出力となるフィードテキストの生成に責務を持つ
- 3つの異なる集約タイプ
- no aggregation
- フィードストーリーが1つのイベントから成る
- item aggregation
- フィードストーリーがアクションとオブジェクトを共有するイベントから成る
- 例:Ryan modified Test.psd 4 times
- フィードストーリーがアクションとオブジェクトを共有するイベントから成る
- verb aggregation
- フィードストーリーが共通のアクションから成る
- 例:Ryan modified Test.psd, Homepage.psd, and Wireframes.psd
- フィードストーリーが共通のアクションから成る
- no aggregation
- 何が重要な情報なのかを明確にする
- Text Processing
- フィードストーリーに含まれるファイルやフォルダなどを個別にリンクする必要がある
- 1つのストーリーに異なるアイテムが含まれることがあるので、ストーリー全体をリンクすることはできない
- ストーリーをMarkdownでマークアップする
- raw出力、HTML、プレーンテキストでの出力を保証する
- JSONで便利なようにエンティティの配列を生成するためにもテキストを解析する
- Updating the Feed
- 最新のフィードストーリーから新しいイベントを生成する
- 新しいものだけを再生成すればよい
- Caching
- アクティビティフィードのキャッシュは2段階
- キャッシュキーのスキーマはシンプル
- Redisはappend-onlyモードで毎秒書き込み
- Key Scheme
- アクティビティフィードのキャッシュキー要素
- 現在のユーザー、現在のフィルター(あれば)、現在のページ(あれば)
- activity/#{owner.id}/#{filter.type}:#{filter.id}/#{page_id}
- 最新のアクティビティデータのページにはページ番号は割り当てられない
- それ以降は単純に減っていく
- キャッシュデータは常に次のページのIDを保持している(ページングを容易にするため)
- アクティビティフィードのキャッシュキー要素
- Data Caching
- フィード処理と集約によって生成されたデータは後の参照のためにRedisにストアされる
- アクティビティフィードデータの最新ページにとっては最重要
- そのHTMLはページがリクエストされる度に毎回描画されるため
- データベースを参照しないで済むので、ページ描画が最短となる
- Possible Improvements
- これらの変更はアクティビティフィードの正しい方向への大きな一歩
- 考慮すべき小さな改善もある
- Optimizing Feed Reprocessing
- 新しいイベントが発生すると、アクティビティアイテムの最新ページを再処理成する
- これには長い時間はかからないが、実際には、ストーリーの重みを実装するまでは最新ページの最後のアクティビティについてのみ考慮すればよい
- Story Weighting
- 現在はすべてのストーリーは同じ重要度として扱われる
- 将来は、各タイプのストーリーを重み付けして、重要なアクションはフィードの上位に来るようにする
- Object Aggregations
- 複数のアクションが同じオブジェクト(またはオブジェクトのセット)に行われた場合、それをひとまとめにできる
- Ryan created and modified Test.psd のような場合にバッチプロセスはそれをうまく扱えない
- Realtime Events
- アクティビティフィードに複雑さをもたらすため、リアルタイムイベントは無効になっている
- DOM Node Caching
- とても古いイベントまですくローrうすると、大量のノードのDOMへの追加が発生する
- 改善するには、スクロール時に見えなくなるノードをDOMから取り除き、必要に応じて再追加をする必要がある
- これはページネーションを複雑にする