如何管理 App Engine Go 运行时上下文以避免 App Engine 锁定?

Posted

技术标签:

【中文标题】如何管理 App Engine Go 运行时上下文以避免 App Engine 锁定?【英文标题】:How could I manage the App Engine Go runtime context to avoid App Engine lock-in? 【发布时间】:2013-05-25 11:27:31 【问题描述】:

我正在编写一个在 App Engine 的 Go 运行时上运行的 Go 应用程序。

我注意到几乎所有使用 App Engine 服务(例如 Datastore、Mail 甚至 Capabilities)的操作都需要您向其传递 appengine.Context 的实例,该实例必须使用函数 appengine.NewContext(req *http.Request) Context 进行检索。

当我为 App Engine 编写此应用程序时,如果我愿意的话,我希望能够轻松快速地将其移动到其他平台(可能不支持任何 App Engine API 的平台)。

因此,我通过围绕任何 App-Engine 特定交互(包括请求处理函数)编写小包装器来抽象出与 App Engine 服务和 API 的实际交互。使用这种方法,如果我确实希望迁移到不同的平台,我只需重写那些将我的应用程序与 App Engine 绑定的特定模块。简单明了。

唯一的问题是appengine.Context 对象。我无法将它从我的请求处理程序通过我的逻辑层传递到处理这些 API 的模块,而无需将我的所有代码都绑定到 App Engine。我可以传递http.Request 对象,从中可以派生appengine.Context 对象,但这需要耦合可能不应该耦合的事物。 (我认为最好的做法是让我的应用程序都不知道它是一个 Web 应用程序,除了那些专门用于处理 HTTP 请求的部分。)

想到的第一个解决方案是在某个模块中创建一个持久变量。像这样的:

package context

import (
    "appengine"
)

var Context appengine.Context

然后,在我的请求处理程序中,我可以使用 context.Context = appengine.NewContext(r) 设置该变量,并且在直接使用 App Engine 服务的模块中,我可以通过访问 context.Context 来获取上下文。没有任何介入代码需要知道appengine.Context 对象的存在。唯一的问题是"multiple requests may be handled concurrently by a given instance",这可能导致此计划出现竞争条件和意外行为。 (一个请求设置它,另一个设置它,第一个访问它并获取错误的appengine.Context 对象。)

理论上我可以将appengine.Context 存储到数据存储区,但是我必须将一些特定于请求的标识符向下传递到逻辑层到特定于服务的模块,以识别数据存储区中的哪个appengine.Context 对象是用于当前的请求,这将再次耦合我认为不应该耦合的事情。 (而且,它会增加我的应用程序的数据存储使用量。)

我还可以将appengine.Context 对象传递到整个逻辑链,类型为interface,并让任何不需要appengine.Context 对象的模块忽略它。这样可以避免将我的大部分应用程序绑定到任何特定。然而,这似乎也很混乱。

所以,我有点不知所措如何干净地确保需要 appengine.Context 对象的 App-Engine 特定模块可以获取它。希望你们能给我一个我自己还没有想到的解决方案。

提前致谢!

【问题讨论】:

【参考方案1】:

这很棘手,因为您自己强加的范围规则(这是一个明智的规则)意味着不传递 Context 实例,并且没有任何类似于 Java 的 ThreadLocal 可以通过偷偷摸摸的方式达到相同的目的。这实际上是一件好事,真的。

Context 将日志记录支持(简单)与 Call 结合到 appengine 服务(不容易)。我认为有十个 appengine 函数需要Context。除了将所有这些包裹在您自己的门面后面之外,我看不到任何干净的解决方案。

有一件事可以帮助您 - 您可以在您的应用中包含一个配置文件,使用某种标志来指示它是否在 GAE 中。您的全局布尔值只需要存储此标志(不是共享上下文)。在决定是使用 NewContext(r) 获取 Context 来访问 GAE 服务,还是使用相似结构来访问您自己的替代服务时,您的外观函数可以参考此标志。

编辑:最后,当你解决这个问题时,我可以邀请你分享你是如何做到的,甚至可能是一个开源项目?我厚颜无耻地问,但如果你不问...... ;-)

【讨论】:

感谢您的回复!我想我有一个想法可能适用于我的特定应用程序。我不确定它是否能正常工作,但可能适用于相当大比例的 App Engine 应用程序。如果我的想法最终成为一个半通用的解决方案,我肯定会将它作为一个开源项目发布。 :D 我的大部分应用程序都可以访问用户 ID 或会话 ID。我的计划只是创建一个持久映射,其键值直接来自会话 ID 或用户 ID。请求处理程序可以创建和存储上下文对象,需要上下文的模块可以类似地检索它们。不过,我不完全确定它会成功。 (可能有些模块无法访问会话 ID 或用户 ID。在某些情况下,我可能需要发挥创造力。)【参考方案2】:

我(希望)通过像这样包装我的请求处理程序(在本例中称为“realHandler”)解决了这个问题:

http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) 
    ds := NewDataStore(r)
    realHandler(w, r, ds)
)

NewDataStore 创建一个 DataStore,它是一个抽象 GAE 数据存储的简单包装器。它有一个未公开的字段,用于存储上下文:

type DataStore struct 
    c appengine.Context


func NewDataStore(req *http.Request) *DataStore 
    return &DataStoreappengine.NewContext(req)

在请求处理程序中,我可以在需要时访问抽象的数据存储,而不必担心已经设置的 GAE 上下文:

func realHandler(w http.ResponseWriter, req *http.Request, db *DataStore) 
    var s SomeStruct
    key, err := db.Add("Structs", &s)
    ...

【讨论】:

【参考方案3】:

特别是在 Datastore 的情况下,您应该能够在不同的请求中重用相同的 appengine.Context。我自己没有尝试过这样做,但这就是名为 goon 的 Datastore 替代 API 的工作方式:

Goon 与 datastore 包有很多不同之处:它记住 appengine 上下文,只需要在创建时指定一次

顺便说一句,存储应该依赖于 HTTP 请求的事实听起来很荒谬。我不认为 Datastore 依赖于通常意义上的 特定 请求。最有可能的是,需要识别一个特定的 Google App Engine 应用程序,该应用程序显然在请求之间保持相同。我的推测是基于对google.golang.org/appengine 的source code 的快速浏览。

您可以对其他 Google App Engine API 进行类似观察。当然,所有细节可能是特定于实现的,在实际应用中实际使用这些观察之前,我进行了更深入的研究。

【讨论】:

【参考方案4】:

值得注意的是,Go 团队引入了一个 golang.org/x/net/context 包。

后来在托管虚拟机中提供了上下文,repo 是here。文档指出:

此存储库支持 App Engine 上的 Go 运行时,包括经典 App Engine 和托管虚拟机。它提供用于与 App Engine 服务交互的 API。它的规范导入路径是google.golang.org/appengine

这意味着您可以根据appengine轻松地在开发环境之外编写另一个包。

特别是像appengine/log(普通日志包装器example)这样的包变得非常容易包装。

但更重要的是,这允许人们在表单中创建处理程序:

func CoolHandler(context.Context, http.ResponseWriter, *http.Request)

Go 博客 here 上有一篇关于 context 包的文章。我写了关于使用上下文here。如果您决定使用处理程序来传递上下文,那么最好在一个地方为所有请求创建上下文。您可以使用非标准的请求路由器,如github.com/orian/wctx。

【讨论】:

【参考方案5】:

我通过将appengine.NewContext 包装在一个接口后面来处理这个问题,并通过不同的包提供不同的实现。这样,我就不必将 GAE 链接到任何不使用它的二进制文件中:

type Context interface 
  GetHTTPContext(r *http.Request) context.Context

我为子包提供了一种在导入副作用时注册自己的方法,类似于database/sql-style:

var _context Context
func Register(c Context) 
  _context = c // Nil checking, double registration checking omitted for brevity

我从一个普通的、非 GAE 二进制文件的默认实现开始,它只是抓取现有的上下文:

var _context Context = &defaultContext // use this by default
type defaultContext struct 
func (d *defaultContext) GetHTTPContext(r *http.Request) context.Context 
  return r.Context()

然后我将一个 App Engine 实现放在一个包中 mything/context/appengine:

import(
  ctx "mything/context"
)

type aecontext struct 
func (a *aecontext) GetHTTPContext(r *http.Request) context.Context 
  return appengine.NewContext(r)


func init() 
  ctx.Register(&aecontext)

然后我的 GAE 二进制文件可以拉入子包,它在init 中注册自己:

import(
  _ "mything/context/appengine"
)

我的应用代码使用GetHTTPContext(r) 来获取适当的上下文以传递给依赖项。

【讨论】:

以上是关于如何管理 App Engine Go 运行时上下文以避免 App Engine 锁定?的主要内容,如果未能解决你的问题,请参考以下文章