在API库中的后台获取

huangapple go评论84阅读模式
英文:

Background Fetch in an API Library

问题

我正在编写一个API客户端(库),用于访问一个JSON端点并填充内存缓存。

到目前为止:

  • 在库的init()函数中,我启动了一个time.Ticker循环,每分钟访问一次API,刷新缓存(一个嵌入了JSON结构和时间戳的结构体)。
  • 库中的公共函数调用只是从缓存中获取数据,因此它们不需要担心自己的速率限制,但是可以检查时间戳以确认数据的新鲜度。

然而,在init()中启动time.Ticker感觉不太对:我没有看到其他库这样做。但是,我确实希望避免包用户为了从几个JSON端点获取数据而做大量的工作。

我的公共API如下所示:

// 示例用法:
// rt := api.NewRT()
// err := rt.GetLatest
// tmpl.ExecuteTemplate(w, "my_page.tmpl", M{"results": rt.Data})

func (rt *RealTime) GetLatest() error {
    rt = realtimeCache.Cached
    if rt == nil {
        return errors.New("没有可用的缓存响应。")
    }

    return nil
}

内部的获取器如下所示:

func fetchLatest() error {
    log.Println("获取最新的实时结果。")
    resp, err := http.Get(realtimeEndpoint)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    // 锁定我们的缓存以进行写操作
    realtimeCache.Lock()
    defer realtimeCache.Unlock()

    var rt *RealTime
    err = json.Unmarshal(body, &rt)
    if err != nil {
        return err
    }

    // 更新缓存
    realtimeCache.Cached = rt

    return nil
}

func init() {
    // 在启动时填充缓存
    fetchLatest()
    fetchHistorical()

    // 每分钟刷新缓存(默认)
    ticker := time.NewTicker(time.Second * interval)
    go func() {
        for _ = range ticker.C {
            fetchLatest()
            fetchHistorical()
        }
    }()
}

API的其他部分也有类似的函数(我正在模块化,但一开始我保持简单),但这就是要点。

有没有更好的方法来让后台工作程序获取结果,同时仍然对用户友好?

英文:

I'm writing an API client (library) that hits a JSON end-point and populates an in-memory cache.

Thus far:

  • I kick off a time.Ticker loop in the library's init() function that hits the API every minute, which refreshes the cache (a struct that embeds the JSON struct and a timestamp).
  • The public facing function calls in the library just fetch from the catch and therefore don't need to worry about rate-limiting on their own part, but can check the timestamp if they want to confirm the freshness of the data

However, starting a time.Ticker in init() does not feel quite right: I haven't seen any other libs do this. I do however want to avoid the package user having to do a ton of work just to get data back from few JSON endpoints.

My public API looks like this:

// Example usage:
// rt := api.NewRT()
// err := rt.GetLatest
// tmpl.ExecuteTemplate(w, "my_page.tmpl", M{"results": rt.Data})

func (rt *RealTime) GetLatest() error {
	rt = realtimeCache.Cached
	if rt == nil {
		return errors.New("No cached response is available.")
	}

	return nil
}

And the internal fetcher is as below:

func fetchLatest() error {
	log.Println("Fetching latest RT results.")
	resp, err := http.Get(realtimeEndpoint)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	// Lock our cache from writes
	realtimeCache.Lock()
	defer realtimeCache.Unlock()

	var rt *RealTime
	err = json.Unmarshal(body, &rt)
	if err != nil {
		return err
	}

	// Update the cache
	realtimeCache.Cached = rt

	return nil
}

func init() {
	// Populate the cache on start-up
	fetchLatest()
	fetchHistorical()

	// Refresh the cache every minute (default)
	ticker := time.NewTicker(time.Second * interval)
	go func() {
		for _ = range ticker.C {
			fetchLatest()
			fetchHistorical()
		}
	}()
}

There are similar functions for other parts of the API (which I'm modularising, but I've kept it simple to start with), but this is the gist of it.

Is there a better way to have a background worker fetch results that's still user-friendly?

答案1

得分: 3

在我看来,启动init()函数的计时器是一个不好的主意,因为你的API的用户应该决定何时以及是否进行获取/缓存/更新。

我建议使用NewRT()函数中的选项或者一个全局布尔值(api.AutoUpdateapi.Caching)来使数据的缓存和自动更新变为可选。

在调用访问器时,你可以采取适当的操作:

  • 如果没有启用缓存,则获取数据
  • 如果启用了缓存但未启用自动更新,则检查数据的新鲜度,如果需要则刷新
  • 如果启用了缓存和自动更新,则不进行任何操作,因为你在NewRT()函数中启动的计时器会为你处理数据

这样一来,在用户需要之前,你不会开始获取任何东西,但是可以灵活地让用户决定是否需要额外的功能。

请注意,在相应的结构被移除后,你应该确保不会保留不必要的计时器。

英文:

IMHO, starting the timer on the init() function is a bad idea, for the single reason that the user of your API should be the one to decide if and when to do the fetching/caching/updating.

I would advise to make optionnal the caching and auto-updating of the data using either options in the NewRT() function or a package-wide boolean (api.AutoUpdate, api.Caching).

On the call of your accessors, you can then make the proper action:

  • Retrieve the data if caching isn't enabled
  • Check for data freshness if caching is enabled but auto-update isn't, and refresh if needed
  • Nothing if caching and auto-updating is enabled, as your timer (started in the NewRT() function) will take care of the data for you

This way you don't start retrieving anything before your user need it, but have the flexibility to let your user decide if they need additionnal functionnalities.

Note that you should ensure that unnecessary timers aren't kept after the corresponding struct have been removed.

答案2

得分: 1

像Elwinar说的那样,在init函数中启动计时器是一个不好的主意,但是你有一个构造函数,所以任何"对象构造"都应该在其中发生,这里有一个简短的示例

(请查看完整代码的playground)

func NewRT(interval int) (rt *realTime) {
	rt = &realTime{
		tk: time.NewTicker(time.Second * time.Duration(interval)),
	}
	go func() {
		rt.fetch()
		for _ = range rt.tk.C {
			rt.fetch()
		}
	}()

	return
}

func (rt *realTime) fetch() {
	rt.Lock()
	defer rt.Unlock()
	rt.fetchLatest()
	rt.fetchHistory()
}

......

func (rt *realTime) GetLatest() error {
	rt.RLock()
	defer rt.RUnlock()
	if rt.cached == nil || len(rt.cached) == 0 {
		return ErrNoCachedResponse
	}

	return nil
}

func (rt *realTime) Stop() {
	rt.Lock()
	defer rt.Unlock()
	rt.tk.Stop()
}
英文:

Like Elwinar said, starting the timer in init is a bad idea, however you have a constructor, so any "object construction" should happen in it, here's a short example :

(check the playground for the full code)

func NewRT(interval int) (rt *realTime) {
	rt = &realTime{
		tk: time.NewTicker(time.Second * time.Duration(interval)),
	}
	go func() {
		rt.fetch()
		for _ = range rt.tk.C {
			rt.fetch()
		}
	}()

	return
}

func (rt *realTime) fetch() {
	rt.Lock()
	defer rt.Unlock()
	rt.fetchLatest()
	rt.fetchHistory()
}

......

func (rt *realTime) GetLatest() error {
	rt.RLock()
	defer rt.RUnlock()
	if rt.cached == nil || len(rt.cached) == 0 {
		return ErrNoCachedResponse
	}

	return nil
}

func (rt *realTime) Stop() {
	rt.Lock()
	defer rt.Unlock()
	rt.tk.Stop()
}

huangapple
  • 本文由 发表于 2014年6月24日 06:34:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/24375671.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定