lestrrat-go/httprc
GitHub: lestrrat-go/httprc
一个 Go 语言的 HTTP 资源内存缓存库,通过后台定期刷新机制自动保持远程资源的最新状态。
Stars: 20 | Forks: 8
# github.com/lestrrat-go/httprc/v3  [](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工具, 内存缓存, 开发工具库, 异步刷新, 日志审计