Lokka の wysiwyg エディターは jQuery ベースの jwysiwyg といつやつが採用されている。

jwysiwyg/jwysiwyg

jwysiwyg/jwysiwyg

WYSIWYG jQuery Plugin. Contribute to jwysiwyg/jwysiwyg development by creating an account on GitHub.

github.com

しかしここ最近はメンテナンスされてなくて、最近の Chrome では利用できない状況だったりする。以前、ファイルアップロード機能を付けたときも jwysiwyg の不具合が原因で wysiwyg モードでの画像アップロードは断念した。

Lokka にファイルアップロード機能を付ける

Lokka にファイルアップロード機能を付ける

ブログの管理画面にファイルアップロード機能をつけてみた。 GitHub の Issue みたいにドラッグアンドドロップでアップロードしてくれる。こんな感じ。いまのところ埋め込みフォーマットは Markdown にしか対応してない。Lokka は heroku とか PaaS...

portalshit.net

最近のモダンな wysiwyg はどんなのがあるんだろうと調べてみたら Quill ってのが GitHub で 18000 くらいスター付いてて良さそうだった。

quilljs/quill

quilljs/quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility. - quilljs/quill

github.com

Quill - Your powerful rich text editor

Quill - Your powerful rich text editor

Quill is a free, open source WYSIWYG editor built for the modern web. Completely customize it for any need with its modular architecture ...

quilljs.com

ちょっと試してみたところ wysiwyg で画像のアップロードもできるようになったので jwysiwyg からこいつに置き換えるのも悪くはなさそう。

ただ Quill は擬似 textarea なので実際の form には対応していない。 SPA で利用されることが想定されており、 Ajax 前提なつくりとなっている。サーバーサイドで HTML をレンダリングする昔ながらのウェブアプリで使うには一手間必要そうだった( form の onsubmit で texarea を createElement して Quill で入力したデータをぶっこむなど)。

追記

Pull Request 出した。

Replace jwysiwyg with quill by morygonzalez · Pull Request #237 · lokka/lokka

Replace jwysiwyg with quill by morygonzalez · Pull Request #237 · lokka/lokka

closes #233 Drop jwysiwyg, instead introduce Quill Remove unused css files Add npm kind of things Add rake task to build JavaScript fil...

github.com

さらに追記

↑の Pull Request 、 Merge されたんで Lokka の wysiwyg エディターは Quill となりました。

IMG_4652.jpg

302 Found

302 Found

blog.craftz.dog

↑の記事を読んで tmux でウィンドウを上下分割して、上でコード書いて下でテスト実行したりするの便利そうだなと思ってまねしてみてる。とてもよい。

ただ分割比を調整するために毎回何度かキー入力が発生するのがいまいちだなと思っていた。 tmux のセッションを開始するときにしかこのコマンドを実行することがないので面倒だなと思いながら都度手動で調整していた。

しかし tmux には select-layout という機能があって、 Control
+b + Alt+1 とか入力するといい感じに pane を配置し直してくれることを思い出した。上下分割は C-b + M-3main-horizontal というスタイルが割り当てられており、上の pane をメインにして上下分割してくれる。このとき main-pane-height という設定項目に任意の値を設定しておくと分割した pane の高さを指定できるようだった。デフォルトだとメインの pane の高さは小さめなので自分は set-window-option -g main-pane-height 60 にしておいた。これで tmux を起動してウィンドウを水平分割したあと C-b + M-3 とやるだけで 8:2 くらいで二つの pane に水平分割されるようになった。ちなみに左右で分割したときの値を調整したい場合は set-window-option -g main-pane-width 230 などとしておけばよい。便利。

Facebook にウェブサイトの URL をはっつけるとき参照される HTML メタ情報の仕組みに Open Graph Protocol ってのがある。 Facebook に URL を貼ると bot が URL の内容を読みに行ってページの概要や画像を取得し Facebook 内に埋め込み表示するというもの。 Facebook を見ている人はリンク先の内容をクリックする前に概要を把握できるので、リンクをクリックして見たい情報じゃなかった、ということを避けられる。 Facebook が考案して策定した仕組みだけど、 Facebook に限らずいろんなサイトで OGP タグを出力してるし読み込んでる。 Twitter にも似た仕組みあって Twitter Card という。この辺の対応は結構前にやってた。

アドベントカレンダーに備えて Open Graph protocol に対応

アドベントカレンダーに備えて Open Graph protocol に対応

昨日飲みに行って今朝起きてからふとコード書きたくなって、アドベントカレンダーもあることだし(去年の Adventar で自分のブログだけ og:image がなくて画像が出てなくて残念だった)、このブログを Open Graph protocol に対応させることにした。T...

portalshit.net

ただ自分のサイトが OGP タグを提供するだけではつまんないなと思ったので自分のブログにペロッと URL を貼ったときに相手先に OGP タグがあればそれを出力するようにしてみた。こんな感じ。

OGP Preview

しかしここで困ったことがあって、↑でリンクしてる Circle のサイトは HTTPS で配信されておらず、単純に Circle のサイトで og:image に指定されている画像を SSL 化されているこのブログで読み込むと Mixed Content になってしまう。せっかく HTTP/2 で配信していたのに台なしになってしまう。またそもそも og:image は Facebook でシャアされることを想定されていることがほとんどなので、画像サイズがデカすぎていい感じにスクエアに表示するためには CSS の小技を駆使したりする必要があった。

いい感じに解決する方法ないかなと調べていたら良いのが見つかった。

willnorris/imageproxy

willnorris/imageproxy

imageproxy - A caching, resizing image proxy written in Go

github.com

Go で書かれた Image Proxy Server で、 HTTPS Proxy は当然のこと動的リサイズもできる。使い方は簡単でバイナリを落としてきて動かすだけ。 Go なんで ImageMagick をどうしたりとかを考えなくて良い。 そもそも Docker イメージも提供されているので Docker をインストール済みなら docker run するだけでも動く。 めっちゃお手軽。

こいつのおかげで HTTPS で配信されていないサイトの OGP タグを読み込んでも Mixed Content にならずに済むようになった。また og:image は適切にリサイズできるようになった。画像変換サーバーとかは結構難しいいやつで個人のブログでこんなに簡単に使えるものだとは思ってなかったので正直ビックリした。

AWS の登場で大企業じゃなくても CDN 使ったり仮想サーバーでウェブシステムを構築したりできるようになった。さらには Go や Docker といった技術のおかげで複数の込み入ったソフトウェアを組み合わせて構築していく必要があったシステムが、まるで jQuery を使うような感覚でポン付けで使える時代になってきている。とても素晴らしい。

ちなみに OGP の取得には open_graph_reader という gem を使っている(昔からある opengraph という gem はメンテナンスされておらず最近の Nokogiri で動かない)。 open_graph_reader の作者が結構 Opinionated な人で、以下のような Anti-featurs を掲げている。

open_graph_reader Anti-features

http://ogp.me/ の仕様に準拠していないサイトのことは完全無視というつくり。個人的にはこういう思想は好みだが、現実問題として使い勝手が悪い。例えば hitode909 さんのブログの OGP タグを取得しようとしたところ以下のようなエラーを出して取ってくれなかった。

スクリーンショット 2018-05-26 10.08.47.png

article:published_time は ISO8601 形式の datetime であるべき、とのこと。はてなブログはかなりシェアが大きくリンクする機会が多いので残念。

GW 中、十分インスタンスを用意しておいたが想定を超えるアクセスがあって負荷が高まり、 Alert が飛んでくる事態となった。車を運転中に iPhone をカーステにつないでいたところ Slack がピコピコ鳴り、嫁さんから「休みなのか仕事なのかハッキリしろ!」と言われたので Alert が上がらないようにオートスケールを仕込むことにした。 いみゅーたぶるいんふらすとらくちゃー諸兄からしたら「そんなの常識じゃん」みたいな話ばかりだけど、自分でやってみて得られた知見をまとめておきます。

なおここで言っているのは EC2 インスタンスのオートスケール( EC2 Auto Scaling )であり、 AWS の様々なリソースを包括的にオートスケールする AWS Auto Scaling とは異なります。

Amazon EC2 Auto Scaling

Amazon EC2 Auto Scaling

aws.amazon.com

Auto Scaling (仮想サーバー処理能力の自動拡張・縮小機能) | AWS

Auto Scaling (仮想サーバー処理能力の自動拡張・縮小機能) | AWS

AWS Auto Scaling が、アプリケーションをモニタリングし、安定した予測可能なパフォーマンスを可能な限り低コストで維持するよう自動的に容量を調整する仕組みを確認してください。

aws.amazon.com

オートスケールをやるにあたって必要なこと

1. インスタンス起動時に最新のコードを pull してきてアプリケーションを起動させる

オートスケールしてきたインスタンスだけコードが古いとエラーが発生する。

2. インスタンス停止時にアプリケーションのログファイルをどっか別のところに書き出す

Auto Scaling Group のインスタンスは Stop ではなく Terminate されるため、インスタンス破棄後もログを参照できるように S3 に上げるとかして永続化させる必要がある。 Fluentd や CloudWatch Logs に集約するのでも良い。

3. AMI を定期的にビルドする

オートスケール対象のアプリケーションは枯れていて今更新しいミドルウェアが追加されたりすることはなくてソースコードを git clone してくるだけで十分なのだが、 Gemfile に変更があった場合を想定して少しでもサービスインを早めるため( bundle install を一瞬で終わらせるため)、 master ブランチへの変更が行われなくなる定時間際のタイミングで Packer でビルドして AMI にプッシュするようにしている。

Packer by HashiCorp

Packer by HashiCorp

Packer is a free and open source tool for creating golden images for multiple platforms from a single source configuration.

www.packer.io

4. Launch Configuration を自動作成

AMI のプッシュが成功したら最新の AMI を利用する Launch Configuration を作成し、 Auto Scaling Group も最新の Launch Configuration を参照するように変更する。 AWS CLI でできるので自動化してある。

5. Auto Scaling Group の設定をいい感じにやる

Auto Scaling Policy を決め( CPU 使用率が一定水準を超えたらとか、 Load Balancer へのリクエスト数が一定以上になったらとか)、時間指定で Desired Count や Minimum Count を指定したければ Schedule をいい感じに組む。 AWS Management Console 上でポチポチするだけでよい。

6. deploy 対象の調整を頑張る

当初は Auto Scaling Group のインスタンスには deploy を行わない(業務時間中はオートスケールしない、夜間と土日だけオートスケールさせる)つもりだった。

8bd51139504996f811a14c1dd04e4c25.jpeg

しかしメトリクスを確認すると朝の通勤時間帯や平日の昼休み時間帯などにもアクセス数が多いことがわかったので一日中 Auto Scaling Group インスタンスを稼働させることにした。となると deploy 対象が動的に増減する、ということなので Capistrano の deploy 対象もいい感じに調整しないといけない。 AWS SDK Ruby で稼働中の EC2 インスタンスの情報はわかるので、 deploy 時には動的に deploy 対象を判定するようにした。

auto-scaling-all-day.png

本当は push 型 deploy をやめて pull 型 deploy にするのがナウでヤングなのだろうが、レガシーアプリケーションに対してそこまでやるのは割に合わない。そのうちコンテナで動くもっとナウでヤングなやつに置き換えるのでこういう雑な対応でお茶を濁すことにした。

注意点

冒頭に書いているけどあくまで上記は EC2 インスタンスの Auto Scaling であり、周辺のミドルウェアは Scaling されない。例えば RDS を使っていたとして、 RDS インスタンスの方は拡張されないので Connection 数が頭打ちになったり、 CPU を使うクエリが沢山流れたりしたらそこがボトルネックになって障害になってしまう。周辺ミドルウェア、インフラ構成に余力を持たせた状態で行う必要がある場合は AWS Auto Scaling の方を使うことになると思う。

所感

1 と 2 のステップはすでに実現できていたので、自分は 3 、 4 、 5 、 6 をやった。オートスケール、めっちゃむずかしいものというイメージを持っていたけど、まぁまぁすんなり行った(二日くらいで大枠はできて、連休後半には実戦投入した)。負荷に応じて EC2 インスタンスがポコポコ増えて、週末の夜にパソコンを持たずに出かけられるようになった。これで家庭円満です。

Circle 2018

CIRCLE '18 2018年5月12日(土) - 5月13日(日)福岡・海の中道海浜公園野外劇場

CIRCLE '18 2018年5月12日(土) - 5月13日(日)福岡・海の中道海浜公園野外劇場

CIRCLE '18 2018年5月12日(土) - 5月13日(日)に福岡県海の中道海浜公園 野外劇場にてCIRCLE '18が開催決定!

circle.fukuoka.jp

昨年、 Sunset Live 2017 に行ってペトロールズのことを好きになり( 3 月のライブにも行った)、 Circle に彼らがやってくるというので海の中道海浜公園まで行ってきた。

Circle 、 Sunset Live のような感じなのかと思っていたけど非常に快適でよかった。

Circle の良い点としては以下がある。

  • 来てる人が色白で体感治安がよい。夏のあとのフェスだとみんな真っ黒に日焼けしてて遊び人風情がある。音楽が好きというよりお祭り好きでやってきてとりあえず服を脱いで異性の目を引くような男女がいないのが良い。みんなちゃんと音楽を聞きに来ている感じがする。
  • 入場時に手荷物検査があったりしないのがよい。スタッフが高圧的でない。お客さんにぐでんぐでんに酔っ払って暴れてる人とかいないので高圧的になる必要がないのだろう。
  • ステージの合間に地元ローカルタレントによる MC が入らないのがよい。淡々とアーティストが出てきて演奏していく。
  • 出演アーティストがよい。カクバリズムの人たちが沢山出ててシャレオツだった
  • バスが混んでおらず異常な並び方をしなくて良いのが良い。会場までも渋滞などすることなくサクッと着いた。バスは綺麗な車輌で快適そのものだった。通常の路線バスにぎゅうぎゅうに押し込まれ渋滞地獄のなか向かわないといけなかったりすると本当に最悪。
  • 混雑具合が程よく、疲れたら後ろの方のござエリアやテントエリアから座って音楽を聴くことができるのが良い。キャンプ椅子やテーブルを持ち込めるのもよい。

トイレの配置が偏っててめっちゃ並ぶところと空いてるところがあるのが困ったが、それ以外は特に問題がなく快適な野外音楽イベントだった。ぜひまた来年も行きたい。

A summer storm

問題点

Rails でデファクトスタンダードとなっているページネーション gem に Kaminari というのがある。

kaminari/kaminari

kaminari/kaminari

kaminari - ⚡ A Scope & Engine based, clean, powerful, customizable and sophisticated paginator for Ruby webapps

github.com

めっちゃ最高便利で大好きなのだけど、巨大なテーブルに対して COUNT 文を投げると遅いという問題にぶち当たった。このような巨大なテーブルで Kaminari を使うために COUNT 文を発行しない without_count というメソッドが用意されている( Kaminari 1.0.0 でやってくる 5 つの大きな変更 - Qiita )が、これを使うと next_pageprev_pagetotal_pages が取れなくなる(当たり前)。次のページがあるかどうかはばくち状態になってしまう。

本当は DB のスキーマを見直すべき(インデックスがちゃんと効くようにスキーマ変更するべき)だが、 Rails からもレガシーアプリからも同時に同じ DB にアクセスしており、並行運用しているような状況ではなかなか大胆な変更は実行できない。

DB 構造をなおせないとなるとキャッシュを思いつく。 HTML も Rails でレンダリングするのであれば partial cache などでページャー部分だけをキャッシュすれば良いが、 API 選任野郎と化した Rails ではビューのキャッシュはできない。

どうしたか

total_count をキャッシュする。公開範囲を設定できるようなリソースだと全員同一のキーでキャッシュするわけにはいかないのでユーザーごとにキーを作ってキャッシュする必要あり。全ユーザーの全リクエストでスロークエリになってたやつが 5 分に一回スロークエリになるくらいだったら何とか許容できる。

例えば以下のようなコントローラーがあったとする。 HeavyModel には数千万レコードあって、普通に COUNT 文を投げると遅い。 Paginatable という名前でモジュールを定義して、 render メソッドを上書きし、ページネーションを間に挟み込む。

class HeavyModelController < ApplicationController
  include Paginatable

  before_action :login_required

  def index
    resources = HeavyModel.all
    render json: resources, paginate: true
  end
end

モジュールはこんな感じ。車輪の再発明をしている感はあるが、 COUNT 文の結果が current_user 、コントローラーのクラス名、アクション名のそれぞれをつなげたものをキーにしてキャッシュされる。

module Paginatable
  def render(*args)
    options = args.extract_options!
    resources = options[:json]
    if options[:paginate]
      resources, meta = paginate(resources, cache_total_count: options[:cache_total_count])
      options[:json] = resources
      options[:meta] ||= {}
      options[:meta].merge!(meta)
    end
    args << options
    super(*args)
  end

  def paginate(resources, options = {})
    parse_params_for_pagination
    paginator = Paginator.new(
      resources:         resources,
      page:              @page,
      per:               @per,
      cache_total_count: options[:cache_total_count],
      cache_key:         total_count_cache_key
    )
    [paginator.resources, paginator.meta]
  end

  def total_count_cache_key
    @total_count_cache_key ||= "#{current_user&.id}_#{self.class.name}_#{action_name}_count"
  end

  class Paginator
    attr_reader :cache_total_count

    UNCOUNTABLE = -1

    def initialize(resources:, page:, per:, cache_total_count: false, cache_key: nil)
      @_resources        = resources
      @page              = page.to_i
      @per               = per.to_i
      @cache_total_count = cache_total_count
      @cache_key         = cache_key
    end

    def resources
      @resources ||= if cache_total_count?
                       @_resources.page(page).per(per).without_count
                     else
                       @_resources.page(page).per(per)
                     end
    end

    def page
      @page.zero? ? 1 : @page
    end

    def per
      @per.zero? ? Kaminari.config.default_per_page : @per
    end

    def meta
      {
        current_page: current_page,
        next_page:    next_page,
        prev_page:    prev_page,
        total_pages:  total_pages,
        total_count:  total_count
      }
    end

    alias cache_total_count? cache_total_count

    def paginatable?
      !cache_total_count? && resources.respond_to?(:total_count)
    end

    def current_page
      resources.current_page || page
    end

    def next_page
      paginatable? ? (resources.next_page || UNCOUNTABLE) : next_page_fallback
    end

    def next_page_fallback
      return UNCOUNTABLE if page < 1
      return UNCOUNTABLE if per > resources.length
      total_count_fallback > current_page * per ? current_page + 1 : UNCOUNTABLE
    end

    def prev_page
      paginatable? ? (resources.prev_page || UNCOUNTABLE) : prev_page_fallback
    end

    def prev_page_fallback
      return UNCOUNTABLE if page < 2
      (total_count_fallback.to_f / per).ceil >= page ? current_page - 1 : UNCOUNTABLE
    end

    def total_pages
      paginatable? ? resources.total_pages : (total_count.to_f / per).ceil
    end

    def total_count
      paginatable? ? resources.total_count : total_count_fallback
    end

    def total_count_fallback
      @total_count_fallback ||=
        begin
          cached_total_count = Rails.cache.read(@cache_key)
          if cached_total_count
            cached_total_count
          else
            real_total_count = @_resources.page(page).total_count
            Rails.cache.write(@cache_key, real_total_count, expires_in: 5.minutes)
            real_total_count
          end
        end
    end
  end
end

これで 5 分間はキャッシュが効くようになる。

先日書いた Day One のバックエンドで障害 - portal shit! について、 Day One のヘルプページで詳細を説明する記事が掲載されていました。

May 2018 Day One Outage Postmortem | Day One Help

May 2018 Day One Outage Postmortem | Day One Help

help.dayoneapp.com

ウェブアプリケーションエンジニアの皆さんが読むと参考になるのではないかと思い、翻訳の可否をたずね許可をもらったので翻訳します。


2018 年 5 月の Day One 障害報告

執筆者: Paul Mayne (訳注:Day One の創業者)
今週更新(訳注:障害復旧日の午後に公開されました)

2018 年の 5 月 7 日から 10 日にかけて、 Day One は重大な Sync サービスの停止に陥りました。ユーザーの皆さんから堅牢な Sync サービスを期待されていることはわかっていますし、この出来事は我々の基準を満たすものでもありません。何が起こったのか、そして将来にわたってどのようにこの問題を回避していくかをユーザーの皆さんにお伝えしたいと思います。

簡単なまとめ

5 月 7 日にハードウェア障害が発生し、最初の Sync サービスの停止が発生しました。バックアップデータが不完全だったことが原因で、 5 月 8 日の Sync サービスの復旧処理中、一部のユーザーのアカウント ID が既存のユーザーのものと重複してしまいました。結果、一部の新規登録ユーザーは他人の記事を見ることができる状態となっていました。問題に気がついてすぐに Sync サービスを再び停止させました。対象のユーザーは 106 人で、これは我々のユーザー全体の 0.01% よりも少ない値です。

現在、 Sync サービスは正常に復旧しており、記事が他人に見られることはありません。近日中(訳註:すでにリリース済みです)に意図せず共有されてしまった記事を削除するアップデートを提供します。

End-to-end の暗号化を施していた記事は今回の事故の対象外です。

いかなる偶発的な個人情報の漏出も信用を毀損するものであると我々は認識しています。この不幸な状況にあって我々は、正直に状況を説明することが最善だと思っていますし、また信頼を回復するために全力を尽くしています。今回、個人情報の流出被害にあった 106 人のユーザーには終身の Premium メンバーシップを無償提供し、それぞれ個別に連絡を取っています。自分たちにできるあらゆることを行ってみなさんの信頼を回復していきたいと思っています。

障害の詳細

5 月 7 日月曜日、 Day One の社員が DB サーバーでハードウェア障害が発生していることに気がつきました。問題のあるサーバーをデータベースクラスターから取り除く作業とデータを残りのサーバーに分散する作業を始めました。

データの分散処理に失敗します。これが最初の障害です。すぐに Sync サービスを復旧させたかったため、新しいデータベースクラスターを作成して最新のバックアップデータを読み込むことにしました。新しいクラスターがセットアップされ、バックアップが読み込まれました。

5 月 8 日火曜日の早い時間に復旧処理が完了し、 Sync サービスを再開しました。当初は順調に動いているように見えました。しかし数時間で「自分の記事ではない記事が見える」という問い合わせが何人かのユーザーからありました。これは由々しき事態であり、我々はすぐに Sync サービスを再停止し、これ以上被害が拡大しないようにしました。

この時点で、問題と対応方法の調査を行う間、 Day One Sync を無期限に停止する旨をソーシャルメディアに投稿しました。何が原因なのか、何が起こっているのか、そしてもう問題が起こらないと確信できるまで Sync サービスは再開できませんした。

5 月 9 日水曜日の午前、問題の根本原因を突き止めました。復旧処理に用いたバックアップデータが不完全だったのです。記事データは完全なものでしたが、ユーザーアカウントデータに欠落がありました。具体的には、 3 月 22 日よりも後に作られたユーザーアカウントデータが含まれていなかったのです。その結果これらのユーザーはログインすることができていませんでしたし、特定の記事データが意図せず他人から見えてしまうという問題につながりました。

それぞれの記事データベースは “accountID” というフィールドを持ち、どのアカウントがその記事の所有者であるかを判断しています。全ての記事データは正しく復旧されましたが、ユーザーアカウントデータはそうではなかったため、データベースに所有者が存在しない記事ができてしまいました(例えば “My Travel Journal” という記事は 123456 というアカウントのものだったとしましょう。しかしそのアカウントが存在しなくなってしまったということです)。新しいユーザーアカウントの ID は連番で作られます。復旧されたデータは最新のユーザー情報を含んでいなかったため、 5 月 8 日に新規登録したユーザーは本来よりも小さな値の ID で登録され、既存のユーザーアカウントと重複することになったのです。その結果、これらの新規ユーザーはすでに存在する他人が書いた記事を読めるようになってしまったのです。

5 月 8 日の問題が発生していた期間のうちに、 326 アカウントが正しくない account ID で作成されました。その 326 個の ID のうち、 106 個が別人によって書かれた既存の記事データに結びついていました。 2018 年の 3 月 22 日から 23 日に作成されたアカウントは他人から記事が見られる状態になっていたということです。それらは account ID 1104506 から 1104831 の人たちでした。

現在のところそれらの記事のうちどれくらいが end-to-end の暗号化処理をされていたかはわかっていませんが、 end-to-end の暗号化をしていた記事は今回の問題でも他人に見られることはありませんした。

水曜日の調査の後、元のデータベースでデータの分散処理に失敗する問題を解決することが最善の選択肢だと判断しました。いくつか設定ミスがあり、データベースの負荷が高まる原因になっていることがわかりました。この負荷が原因でデータの分散処理が失敗していました。

水曜日の午後、この問題を修正し、元のデータベースクラスターで分散処理が正しく完了することを確認しました。しかしエンジニアチームとサポートスタッフが万全の態勢で臨めるよう、 Sync サービスの再開を木曜の朝まで遅らせることにしました。山岳時間で 5 月 10 日木曜の午前 8 時、 Sync サービスは再度有効化されました。

同期サーバーには大量の待機処理があったため、それらの処理が終わるまでは少しパフォーマンスに問題があるかもしれませんが、なるべく遅延が発生しないように対処しています。

今後どうするのか?

意図せず共有されてしまった記事を削除する機能が入った Day One.app のアップデート(バージョン 2.6.4 )をすぐにリリースします(訳注:すでにリリース済みです)。このアップデートをインストールすると、未ログイン状態のユーザーの端末からは非公開の記事はすべて削除されます。対象ユーザーはアップデートのインストール後 30 日以内に Day One アカウントにログインすることが必要で、このログインをもって Day One.app はログインユーザーを記事の所有者であると認定し、記事を復元します。 Day One.app は影響を受けるユーザーに対してこの変更内容を通知します。

今後、同様の問題が起こらないように、近々以下の改修を同期サーバーに対して施します。

  1. account ID の新規作成時、すでにその ID で記事が作成されていないかをチェックします
  2. 新規の account ID に対しては、連番の数字に加えてランダム生成された二桁の数字を末尾に付加することにします。将来、同じような問題が発生して連番 ID が若返ってしまったとしても ID の衝突が起こる可能性は非常に小さくなります。
  3. いくらかのユーザーアカウントがバックアップ対象から除外されてしまう問題を修正します。

他人に記事を見られてしまった 106 人のユーザーに対しては真摯に謝罪します。対象のユーザーには終身 Premium メンバーシップを提供するとともに、その他懸念点がないか個別に連絡をとっています。今回、大規模な情報流出は起こっていません。第三者がデータベースに侵入したということもありませんし、すべてのデータは我々のサーバーで安全に保管されています。しかしながら 106 人のユーザーの我々に対する信頼は失われてしまいました。信頼は獲得するものであり、与えられるものではないということを承知していますし、再び皆さんからの信頼を取り戻せる機会を得たいと思っています。ユーザーの皆さんには 2017 年 6 月にリリースした end-to-end の暗号化機能を利用することを推奨します。この機能は今回のような事故やその他の問題があったときにもあなたの個人情報を保護します。

今回の障害で Sync サービスを利用できなかった間、辛抱して下さったユーザーの皆さんに感謝します。皆さんが Day One を使って大事な思い出を記録していることを理解しています。今後、よりよくしていくことをお約束します。信頼を回復するため、全力を尽くします。

— Paul