Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13000] ファイルの概要

このコミットは、Go言語のダッシュボードプロジェクトにおけるコードレビューシステムの一部である、メール送信機能とデータストアへのエンティティ保存に関するバグ修正と改善を目的としています。具体的には、メールの即時送信と、time.Time 型フィールドのゼロ値がデータストアに保存される際に発生する問題を解決しています。

コミット

commit dae2992c98bff181b023f27889c513d89714f5ac
Author: David Symonds <dsymonds@golang.org>
Date:   Tue May 1 11:33:25 2012 +1000

    misc/dashboard/codereview: send mail immediately, and fill in time.Time fields.
    
    If we delay the mail sending, we can't send as the current user.
    If we don't fill in the time.Time fields, datastore.Put will fail
    because the zero time.Time value is out of its range.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6136053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/dae2992c98bff181b023f27889c513d89714f5ac

元コミット内容

misc/dashboard/codereview: send mail immediately, and fill in time.Time fields.

If we delay the mail sending, we can't send as the current user.
If we don't fill in the time.Time fields, datastore.Put will fail
because the zero time.Time value is out of its range.

変更の背景

このコミットには主に2つの背景があります。

  1. メール送信のユーザーコンテキスト維持: 以前の実装では、メール送信にappengine/delayパッケージを使用していました。これは、タスクキューを介して非同期に処理を遅延実行するための機能です。しかし、メール送信を遅延させると、メールが送信される時点でのユーザーコンテキスト(つまり、メールを送信したユーザーの認証情報)が失われるという問題がありました。これにより、メールが「現在のユーザーとして」送信されず、システムからのメールとして扱われるか、あるいは送信自体が失敗する可能性がありました。コードレビューシステムでは、特定のユーザーがアクションを起こした結果としてメールが送信されるため、そのユーザーのコンテキストでメールが送られることが重要でした。

  2. time.Time 型のゼロ値とDatastoreの制約: Google App EngineのDatastore(データストア)は、特定のデータ型に対して制約を持っています。time.Time型の場合、Go言語におけるtime.Timeのゼロ値(time.Time{}、つまり0001-01-01 00:00:00 +0000 UTC)は、Datastoreが許容する日付/時刻の範囲外となることがあります。特に、Datastoreは通常、紀元1年より前の日付をサポートしていません。そのため、新しいエンティティをDatastoreに保存する際に、CreatedModifiedといったtime.Time型のフィールドが明示的に初期化されていないと、それらがゼロ値となり、datastore.Put呼び出しが失敗していました。

これらの問題を解決するため、メールは即時送信されるように変更され、新しいエンティティがDatastoreに保存される際にはtime.Time型のフィールドに適切な初期値(Unixエポックのゼロタイム)が設定されるようになりました。

前提知識の解説

このコミットを理解するためには、以下のGoogle App Engine (GAE) およびGo言語の概念に関する知識が必要です。

  • Google App Engine (GAE): Googleが提供するPaaS (Platform as a Service) で、ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。Go言語を含む複数の言語をサポートしています。
  • appengineパッケージ: GAE上でGoアプリケーションを開発するための基本的なAPIを提供します。アプリケーションのコンテキスト管理、ログ出力、エラーハンドリングなどに使用されます。
  • appengine/datastoreパッケージ: GAEのNoSQLデータベースであるDatastoreを操作するためのAPIを提供します。エンティティの保存 (Put)、取得 (Get)、クエリ (Query) などを行います。
    • datastore.Put(c, key, src): 指定されたエンティティをDatastoreに保存します。cはコンテキスト、keyはエンティティのキー、srcは保存するGoの構造体です。
    • datastore.ErrNoSuchEntity: datastore.Getなどの操作で、指定されたキーに対応するエンティティが見つからなかった場合に返されるエラーです。
  • appengine/mailパッケージ: GAEアプリケーションからメールを送信するためのAPIを提供します。
    • mail.Send(c, msg): 指定されたメールメッセージを送信します。
  • appengine/delayパッケージ: GAEのタスクキューを利用して、関数の実行を非同期に遅延させるためのAPIを提供します。これにより、HTTPリクエストの処理中に時間のかかる操作を実行し、レスポンスをすぐに返すことができます。
    • delay.Func(name, f): 遅延実行可能な関数を定義します。
    • Func.Call(c, args...): 遅延実行可能な関数をタスクキューに追加し、非同期に実行をスケジュールします。
  • appengine/taskqueueパッケージ: GAEのタスクキューを直接操作するためのAPIを提供します。appengine/delayは、このtaskqueueパッケージの上に構築された高レベルな抽象化です。
  • appengine/urlfetchパッケージ: GAEアプリケーションからHTTPリクエストを送信するためのAPIを提供します。
  • time.Time型 (Go言語): Go言語の標準ライブラリtimeパッケージで提供される、日付と時刻を表す構造体です。
    • ゼロ値: Goの構造体は、明示的に初期化されない場合、そのフィールドはそれぞれの型のゼロ値で初期化されます。time.Time型のゼロ値は、time.Time{}で表現され、通常は0001-01-01 00:00:00 +0000 UTCとなります。
    • time.Unix(sec, nsec): Unixエポック(1970年1月1日00:00:00 UTC)からの秒数とナノ秒数に基づいてtime.Time値を生成します。time.Unix(0, 0)はUnixエポックのゼロタイムを表します。

技術的詳細

このコミットの技術的詳細は、Google App Engineの特定の挙動とGo言語の型システムに深く関連しています。

  1. appengine/delayとユーザーコンテキストの喪失: appengine/delayパッケージは、内部的にGAEのタスクキューを利用して関数の実行をスケジュールします。HTTPリクエストのコンテキスト(appengine.Context)は、リクエストのライフサイクルに紐付けられています。delay.Func.Callで関数を遅延実行すると、その関数は元のHTTPリクエストとは異なる新しいタスクとして、後で実行されます。この新しいタスクは、元のリクエストのユーザー認証情報やその他のコンテキスト情報を自動的に引き継ぎません。 特にメール送信の場合、mail.Send関数は、現在のユーザーの認証情報を使用してメールを送信しようとします。しかし、遅延実行されたタスクでは「現在のユーザー」が存在しないか、あるいはシステムアカウントのような別のユーザーとして実行されるため、メールが意図した送信者として送られない、または送信権限の問題で失敗する可能性がありました。このコミットでは、メール送信を即時実行することで、元のHTTPリクエストのコンテキスト内でmail.Sendが呼び出され、適切なユーザーとしてメールが送信されるように修正しています。

  2. time.Timeのゼロ値とDatastoreの範囲外エラー: Go言語のtime.Time型のゼロ値は、0001-01-01 00:00:00 +0000 UTCです。Google App EngineのDatastoreは、日付/時刻型のプロパティに対して、通常は紀元1年より前の日付をサポートしていません。Datastoreの内部表現やインデックスの制約により、この「ゼロ年」に近い日付は無効な値として扱われることがあります。 新しいエンティティをDatastoreに保存する際、もしCreatedModifiedのようなtime.Time型のフィールドがGoの構造体で宣言されているにもかかわらず、コード内で明示的に初期化されていない場合、それらは自動的にGoのゼロ値(0001-01-01...)で初期化されます。このゼロ値を持つエンティティをdatastore.Putで保存しようとすると、Datastoreがその日付を無効と判断し、保存操作が失敗します。 このコミットでは、datastore.ErrNoSuchEntity(つまり、新しいエンティティである場合)のチェックを追加し、その場合にCreatedModifiedフィールドをtime.Unix(0, 0)(Unixエポックのゼロタイム、1970-01-01 00:00:00 +0000 UTC)で明示的に初期化することで、Datastoreが許容する有効な日付範囲内に収まるようにしています。これにより、datastore.Putの失敗を防ぎます。

コアとなるコードの変更箇所

このコミットでは、以下の2つのファイルが変更されています。

  1. misc/dashboard/codereview/dashboard/cl.go:

    • appengine/delayパッケージのインポートが削除されました。
    • sendMailLaterというdelay.Funcで定義された変数が削除されました。
    • handleAssign関数内で、sendMailLater.Call(c, msg)という遅延メール送信の呼び出しが、直接mail.Send(c, msg)に置き換えられました。
    • mail.Sendの呼び出しにエラーハンドリングが追加され、エラーが発生した場合はコンテキストにエラーログが出力されるようになりました。
  2. misc/dashboard/codereview/dashboard/mail.go:

    • handleMail関数内で、datastore.ErrNoSuchEntity(エンティティが存在しない、つまり新規作成の場合)のチェックが追加されました。
    • もしエンティティが新規作成の場合、cl.Createdcl.Modifiedフィールドがtime.Unix(0, 0)で明示的に初期化されるようになりました。

コアとなるコードの解説

misc/dashboard/codereview/dashboard/cl.go の変更

--- a/misc/dashboard/codereview/dashboard/cl.go
+++ b/misc/dashboard/codereview/dashboard/cl.go
@@ -16,7 +16,6 @@ import (
 
 	"appengine"
 	"appengine/datastore"
-	"appengine/delay"
 	"appengine/mail"
 	"appengine/taskqueue"
 	"appengine/urlfetch"
@@ -105,8 +104,6 @@ func (cl *CL) ModifiedAgo() string {
 	return "just now"
 }
 
-var sendMailLater = delay.Func("send-mail", mail.Send)
-
 func handleAssign(w http.ResponseWriter, r *http.Request) {
 	c := appengine.NewContext(r)
 
@@ -196,7 +193,9 @@ func handleAssign(w http.ResponseWriter, r *http.Request) {
 				}
 				// TODO(dsymonds): Use cl.LastMessageID as the In-Reply-To header
 				// when the appengine/mail package supports that.
-				sendMailLater.Call(c, msg)
+				if err := mail.Send(c, msg); err != nil {
+					c.Errorf("mail.Send: %v", err)
+				}
 			}
 		}
 	}
  • import "appengine/delay" の削除: appengine/delayパッケージは、メール送信を非同期に遅延させるために使用されていました。メール送信を即時実行する方針に変更されたため、このパッケージは不要となり削除されました。これにより、コードの依存関係が減少し、シンプルになります。

  • var sendMailLater = delay.Func("send-mail", mail.Send) の削除: sendMailLater変数は、mail.Send関数をappengine/delayのメカニズムでラップし、遅延実行可能にするためのものでした。遅延実行が不要になったため、この変数も削除されました。

  • sendMailLater.Call(c, msg) から mail.Send(c, msg) への変更: これがメール送信ロジックの核心的な変更です。以前はdelay.Funcを介してメール送信がタスクキューに登録され、非同期に実行されていました。この変更により、mail.Send関数がhandleAssign関数内で直接呼び出されるようになりました。これにより、メール送信はHTTPリクエストの処理中に同期的に行われ、リクエストをトリガーしたユーザーのコンテキストが維持されたままメールが送信されるようになります。

  • エラーハンドリングの追加: if err := mail.Send(c, msg); err != nil { c.Errorf("mail.Send: %v", err) } 直接mail.Sendを呼び出すようになったため、その戻り値であるエラーを適切に処理する必要があります。ここでは、エラーが発生した場合にappengine.ContextErrorfメソッドを使用してログに出力しています。これにより、メール送信の失敗を監視し、デバッグすることが可能になります。

misc/dashboard/codereview/dashboard/mail.go の変更

--- a/misc/dashboard/codereview/dashboard/mail.go
+++ b/misc/dashboard/codereview/dashboard/mail.go
@@ -45,6 +45,12 @@ func handleMail(w http.ResponseWriter, r *http.Request) {
 		if err != nil && err != datastore.ErrNoSuchEntity {
 			return err
 		}
+		if err == datastore.ErrNoSuchEntity {
+			// Must set sentinel values for time.Time fields
+			// if this is a new entity.
+			cl.Created = time.Unix(0, 0)
+			cl.Modified = time.Unix(0, 0)
+		}
 		cl.LastMessageID = msg.Header.Get("Message-ID")
 		_, err = datastore.Put(c, key, cl)
 		return err
  • if err == datastore.ErrNoSuchEntity の追加: handleMail関数は、おそらく受信したメールメッセージを処理し、それに関連するコードレビュー(clエンティティ)をDatastoreに保存する役割を担っています。datastore.Getなどの操作でdatastore.ErrNoSuchEntityが返されるのは、対応するエンティティがDatastoreにまだ存在しない、つまり新しいエンティティを作成する必要がある場合です。

  • cl.Created = time.Unix(0, 0)cl.Modified = time.Unix(0, 0) の初期化: 新しいエンティティの場合、cl.Createdcl.Modifiedというtime.Time型のフィールドは、Goのデフォルトのゼロ値(0001-01-01...)で初期化されてしまいます。前述の通り、このゼロ値はDatastoreが許容する日付範囲外であるため、datastore.Putが失敗する原因となります。 この変更では、time.Unix(0, 0)を使用して、これらのフィールドをUnixエポックのゼロタイム(1970-01-01 00:00:00 +0000 UTC)で明示的に初期化しています。この値はDatastoreが有効な日付として認識するため、datastore.Putが正常に実行されるようになります。これにより、新しいエンティティの保存時の堅牢性が向上します。

関連リンク

参考にした情報源リンク

  • Google App Engine Documentation (Go)
  • Go Language time package documentation
  • Go Language appengine/datastore package documentation
  • Go Language appengine/mail package documentation
  • Go Language appengine/delay package documentation