lestrrat-go/httprc

GitHub: lestrrat-go/httprc

一个 Go 语言的 HTTP 资源内存缓存库,通过后台定期刷新机制自动保持远程资源的最新状态。

Stars: 20 | Forks: 8

# github.com/lestrrat-go/httprc/v3 ![](https://github.com/lestrrat-go/httprc/v3/workflows/CI/badge.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/httprc/v3.svg)](https://pkg.go.dev/github.com/lestrrat-go/httprc/v3) `httprc` 是一个 HTTP "刷新" 缓存。它的目标是缓存可以通过 HTTP 获取的远程资源,同时通过定期刷新来保持缓存的内容是最新的。 # Client 一个 `httprc.Client` 对象由 3 部分组成:面向用户的控制器 API、主控制器循环,以及执行实际获取操作的 worker 集合。 面向用户的控制器 API 是当你调用 `(httprc.Client).Start` 时返回的对象。 ``` ctrl, _ := client.Start(ctx) ``` # Controller API 控制器 API 让你能够访问异步运行的控制器后端。所有方法都接收一个 `context.Context` 对象,因为它们可能会发生阻塞。如果你无法容忍阻塞操作,应当小心地使用 `context.WithTimeout` 来合理设置超时时间。 # Main Controller Loop 主控制器循环与控制器 API 异步运行。它是单线程的,并且有两个职责。 第一个职责是接收来自控制器 API 的命令,并适当地修改 goroutine 的状态,例如修改它正在监视的资源列表、执行强制刷新等。 另一个职责是定期唤醒并遍历资源列表,重新获取那些已过 TTL 的资源(实际上,每个资源携带的是一个“下次检查”时间,而不是 TTL)。主控制器循环本身只做这些:它只是定期触发这些检查。 获取操作之间的间隔时间会根据 HTTP 响应携带的元数据(例如 `Cache-Control` 和 `Expires` 头部)动态改变,或者使用用户为特定资源设置的固定间隔。在这些值之间,主控制器循环会选择最短的间隔(但不会少于 1 秒),并根据该值检查资源是否需要更新。 例如,如果资源 A 的过期时间为 10 分钟,而资源 B 的过期时间为 5 分钟,主控制器循环将尝试大约每 5 分钟唤醒一次以检查资源。 当控制器循环检测到某个资源需要检查新鲜度时,它会将该资源发送到 worker 池进行同步。 # 间隔计算 资源同步完成后,会调度下一次获取。到下一次获取的间隔是通过使用固定间隔,或者使用来自 `http.Response` 对象的值进行启发式计算得出的。 如果指定了固定间隔,则不会执行额外的计算。如果你指定了 15 分钟的固定间隔,资源将每 15 分钟检查一次。这是可预测且可靠的,但不一定高效。 如果你没有指定固定间隔,系统将分析 HTTP 响应中 `Cache-Control` 和 `Expires` 头部的值。这些值将与最大和最小间隔值进行比较,两者的默认值分别为 30 天和 15 分钟。如果从头部获取的值落在这个范围内,则使用头部的值。如果该值大于最大值,则使用最大值。如果该值小于最小值,则使用最小值。 # URL 白名单 默认情况下,client 允许所有 URL。如果你存储的资源其 URL 来自不可信的源,你应该通过 `httprc.WithWhitelist` 传递一个白名单来限制可以获取的内容。系统提供了几种实现:`BlockAllWhitelist`、`InsecureWhitelist`(允许所有)、`MapWhitelist`(精确字符串匹配)和 `RegexpWhitelist`。 ## 关于 `RegexpWhitelist` 模式的说明 `RegexpWhitelist` 使用 `(*regexp.Regexp).MatchString` 匹配每个 URL,当模式匹配到 URL 的**任意子字符串**时,它就会返回 true。这些模式**不会**为你自动锚定,因此一个简单的模式可能会允许远超你预期的内容。 请考虑以下两种模式之间的区别: ``` // BAD: unanchored, dots unescaped regexp.MustCompile(`http://example.com`) // GOOD: anchored at the start, dots escaped, host terminated with `/` regexp.MustCompile(`^https://example\.com/`) ``` 未锚定的 `http://example.com` 模式会轻易允许诸如以下的 URL: - `http://example.com.attacker.com/evil` — 真实的主机是 `attacker.com`;该模式只要求 `example.com` 出现在*某处*,并且由于没有尾部 `/`,它不会在主机部分结束时停止匹配。 - `http://attacker.com/?redirect=http://example.com` — 该模式出现在查询字符串中,因此即使主机是 `attacker.com` 匹配也会成功。 - `httpsX//exampleYcom` — `.` 是正则表达式中的“任意字符”元字符,因此点号匹配的内容不止是字面意义上的点号。 要将模式固定到特定的 origin: 1. **锚定起始位置**,使用 `^`,这样匹配必须从 URL 的开头开始。 2. **转义点号** (`\.`),使其仅匹配字面量 `.`。 3. **在主机部分末尾加上 `/`**,这样 `example.com` 就不能被扩展为 `example.com.attacker.com`。 需要注意的几个边缘情况: - 要求带有尾部 `/` 意味着纯粹的 origin `https://example.com`(无路径)将无法匹配。如果你需要允许它,请添加第二个模式,例如 `^https://example\.com$`。 - 如果你的 URL 可能包含端口,请明确允许它,例如 `^https://example\.com(:\d+)?/`。 请参阅 `whitelist_example_test.go` 中的 `ExampleRegexpWhitelist`,以获取关于锚定和未锚定模式之间区别的可运行演示。 # 概览 ``` package httprc_test import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "time" "github.com/lestrrat-go/httprc/v3" ) func ExampleClient() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() type HelloWorld struct { Hello string `json:"hello"` } srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(map[string]string{"hello": "world"}) })) options := []httprc.NewClientOption{ // By default the client will allow all URLs (which is what the option // below is explicitly specifying). If you want to restrict what URLs // are allowed, you can specify another whitelist. // // httprc.WithWhitelist(httprc.NewInsecureWhitelist()), } // If you would like to handle errors from asynchronous workers, you can specify a error sink. // This is disabled in this example because the trace logs are dynamic // and thus would interfere with the runnable example test. // options = append(options, httprc.WithErrorSink(errsink.NewSlog(slog.New(slog.NewJSONHandler(os.Stdout, nil))))) // If you would like to see the trace logs, you can specify a trace sink. // This is disabled in this example because the trace logs are dynamic // and thus would interfere with the runnable example test. // options = append(options, httprc.WithTraceSink(tracesink.NewSlog(slog.New(slog.NewJSONHandler(os.Stdout, nil))))) // Create a new client cl := httprc.NewClient(options...) // Start the client, and obtain a Controller object ctrl, err := cl.Start(ctx) if err != nil { fmt.Println(err.Error()) return } // The following is required if you want to make sure that there are no // dangling goroutines hanging around when you exit. For example, if you // are running tests to check for goroutine leaks, you should call this // function before the end of your test. defer ctrl.Shutdown(time.Second) // Create a new resource that is synchronized every so often // // By default the client will attempt to fetch the resource once // as soon as it can, and then if no other metadata is provided, // it will fetch the resource every 15 minutes. // // If the resource responds with a Cache-Control/Expires header, // the client will attempt to respect that, and will try to fetch // the resource again based on the values obatained from the headers. r, err := httprc.NewResource[HelloWorld](srv.URL, httprc.JSONTransformer[HelloWorld]()) if err != nil { fmt.Println(err.Error()) return } // Add the resource to the controller, so that it starts fetching. // By default, a call to `Add()` will block until the first fetch // succeeds, via an implicit call to `r.Ready()` // You can change this behavior if you specify the `WithWaitReady(false)` // option. ctrl.Add(ctx, r) // if you specified `httprc.WithWaitReady(false)` option, the fetch will happen // "soon", but you're not guaranteed that it will happen before the next // call to `Lookup()`. If you want to make sure that the resource is ready, // you can call `Ready()` like so: /* { tctx, tcancel := context.WithTimeout(ctx, time.Second) defer tcancel() if err := r.Ready(tctx); err != nil { fmt.Println(err.Error()) return } } */ m := r.Resource() fmt.Println(m.Hello) // OUTPUT: // world } ```
标签:Go, Homebrew安装, Ruby工具, 内存缓存, 开发工具库, 异步刷新, 日志审计