メルマガ配信の仕組みを刷新した話
今迄はyamlファイルに必要な情報を記入し、それを元にAWS SQSに送信リストを作り定刻になったらAWS SESで送るって仕組みであった。
テスト送信と本送信のAWS SQSのキューが同じだったので誤送信をしたり、定刻に送信するのをhubot上でsetTimeoutを使って送るというやっつけ仕様であったためheroku dynoが再起動すると送れなかったり、休日もその日に送信時刻を仕込まないと駄目だったりと色々と難があった。
そこで、heroku上にpostgresqlで予約テーブルを作りGoogle Drive上の所定フォルダにhtmlコンテンツを置くとテスト送信まで自動で実施->確認を繰り返し、OKとなったらsubjectと送信日時を入れて予約をする仕組みをslack上で完結できるようにした。
以下は主にhubotで動かしているcoffeesriptサンプル
先ずheroku上のpostgresqlに予約テーブルを準備
- sequelizeを使って以下の様なものを準備
htmlコンテンツのファイル名
送信日時
subject
状態
などなど - stateには
initialize
review
verified
preparing
available
completed
などを持たせて状態管理が出来るようにしておいた
本送信が出来るのはavailable
の状態のときのもののとかにする
本当はmachinaを使ってみたかった
Sequelize = require('sequelize')
db = new Sequelize(process.env.DATABASE_URL)
EmagazineSchedule = db.define('EmagazineSchedule', {
send_at: {type: Sequelize.DATE},
file_id: {type: Sequelize.STRING, allowNull: false},
filename: {type: Sequelize.STRING, allowNull: false, unique: true},
subject: {type: Sequelize.TEXT},
state: {type: Sequelize.STRING, defaultValue: 'initialize'},
}, {
paranoid: true,
underscored: true,
underscoredAll: true,
deletedAt: 'deleted_at',
tableName: 'emagazine_schedules',
})
Google Driveの所定フォルダにhtmlファイルを置いたら予約テーブルにレコードが追加されるようにする
- 所定のフォルダに変更がかかった場合、hubotに通知が来るようにする
Google Driveの設定はこちらを参照 - Googleからの通知が受けれるようにhubot上にendpointを作る
https://***.herokuapp.com/google/drive/notifications/emagazine で受ける
Googleapis = require('googleapis')
drive = Googleapis.drive({version: 'v2'})
SERVICE_ACCOUNT_EMAIL = 'xxx@developer.gserviceaccount.com'
SERVICE_ACCOUNT_KEY = process.env.EMAGAZINE_PEM
SCOPE = [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
"https://www.googleapis.com/auth/drive.apps.readonly",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.metadata.readonly",
"https://www.googleapis.com/auth/drive.readonly"
]
jwt = new Googleapis.auth.JWT(SERVICE_ACCOUNT_EMAIL, null, SERVICE_ACCOUNT_KEY, SCOPE)
jwt.authorize (err, tokens) ->
if err
console.log err
else
jwt.credentials = tokens
module.exports = (robot) ->
robot.router.post "/google/drive/notifications/emagazine", (req, res) ->
drive.changes.get {auth: jwt, changeId: data.body.id}, (err, resp) ->
if (not resp.file.labels.trashed and resp.file.mimeType == 'text/html')
filename = resp.file.title
db.transaction().then (t) ->
EmagazineSchedule.findOrCreate({where: {file_id: resp.file.id, filename: filename}, paranoid: false}, {transaction: t, lock: t.LOCK.UPDATE}).spread (schedule, created) ->
# ここにテスト用の送信リスト作成と送信を実施するコードを入れる
...
.catch (err) ->
t.rollback()
送信リスト(AWS SQSにemailアドレスリストを作る)と送信はコマンドを準備
- これは送信サーバ(バッチサーバ)にコマンドを準備した
- hubotからはsshでこれらのコマンドを呼び出してSTDOUTとSTDERRを受け取って処理を実施するのみ
- バッチサーバからDrive上のコンテンツを取得するのはgoogle-drive-ocamlfuseを使ってmountして使用
metadata_cache_time=60
で設定しているのでテストで再送信する場合は60秒の待ちを作ったりする必要あり
テスト送信
- 送信リストの作成と送信を実施
- テスト送信用のアドレスリストを予め準備しておいた
これもhubotにendpointを作り、どこからでもhttp getで取得出来るようにしておいた
本送信用のリスト作成と本送信
- agendaを使って定期的に予約テーブルを確認して該当するものがあれば実行するようにした
- 送信時刻が判明しているのでしっかりとスケジューリングが出来るのでcronの様に毎分確認とする仕様でなくても良いが、TODOとしている
- EventEmitterでタスクを呼び出すようにしている
- 以下は送信リストを作る箇所のサンプル
agendaで定期的にイベントを呼び出す処理
Agenda = require('agenda')
agenda = new Agenda({db: { address: process.env.MONGOLAB_URI}})
module.exports = (robot) ->
agenda.define 'agenda:emagazine:preparing', (job, done) ->
robot.logger.info '@agenda:emagazine:preparing'
robot.emit 'emagazine:preparing', {execute: true}
done()
agenda.every('5 minutes', 'agenda:emagazine:preparing')
agenda.start()
実際に送信リストを作る処理
- トランザクション分離レベルは状況に合わせて変更
module.exports = (robot) ->
robot.on 'emagazine:preparing', (params) ->
db.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}).then (t) ->
...
slackからの操作
以下の処理が行えるようにした
- 予約テーブル一覧表示
- テスト再送信
- 送信日時/subjectの設定
- 承認
- 予約の取り消し
module.exports = (robot) ->
robot.respond /\bschedule\s+emagazine\s+(show|resend|update|verify|destroy|...)\s+(.+)$/, (msg) ->
...
現在テスト運用中で運用上改修したことなどは今後追記予定