siyuan/kernel/bazaar/stage.go

225 lines
6.6 KiB
Go
Raw Normal View History

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package bazaar
import (
"context"
"errors"
"sync"
"time"
gcache "github.com/patrickmn/go-cache"
"github.com/siyuan-note/httpclient"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
"golang.org/x/sync/singleflight"
)
// cachedStageIndex 缓存 stage 索引
var cachedStageIndex = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6)
type StageBazaarResult struct {
StageIndex *StageIndex // stage 索引
BazaarStats map[string]*bazaarStats // 统计信息
Online bool // online 状态
StageErr error // stage 错误
}
var stageBazaarFlight singleflight.Group
var bazaarStatsFlight singleflight.Group
// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
key := "stageBazaar:" + pkgType
v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
return getStageAndBazaar0(pkgType), nil
})
if err != nil {
return
}
result = v.(StageBazaarResult)
return
}
// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
stageIndex, stageErr := getStageIndexFromCache(pkgType)
bazaarStats := getBazaarStatsFromCache()
if nil != stageIndex && nil != bazaarStats {
// 两者都从缓存返回,不需要 online 检查
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: true,
StageErr: stageErr,
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var onlineResult bool
onlineDone := make(chan bool, 1)
wg := &sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
onlineResult = isBazaarOnline()
onlineDone <- true
}()
go func() {
defer wg.Done()
stageIndex, stageErr = getStageIndex(ctx, pkgType)
}()
go func() {
defer wg.Done()
bazaarStats = getBazaarStats(ctx)
}()
<-onlineDone
if !onlineResult {
// 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
cancel()
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: false,
StageErr: stageErr,
}
}
// 在线时等待所有请求完成
wg.Wait()
return StageBazaarResult{
StageIndex: stageIndex,
BazaarStats: bazaarStats,
Online: onlineResult,
StageErr: stageErr,
}
}
// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
if val, found := cachedStageIndex.Get(pkgType); found {
ret = val.(*StageIndex)
}
return
}
// getStageIndex 获取 stage 索引
func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
ret = cached
err = cacheErr
return
}
var rhyRet map[string]interface{}
rhyRet, err = util.GetRhyResult(ctx, false)
if nil != err {
return
}
bazaarHash := rhyRet["bazaar"].(string)
ret = &StageIndex{}
request := httpclient.NewBrowserRequest()
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json" // pkgType 单词为复数形式
resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
if nil != reqErr {
logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
err = reqErr
return
}
if 200 != resp.StatusCode {
logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
err = errors.New("get stage index failed")
return
}
cachedStageIndex.SetDefault(pkgType, ret)
return
}
// getStageRepoByURL 根据 pkgType 与 urlowner/repo@hash获取 StageRepo
func getStageRepoByURL(ctx context.Context, pkgType, url string) *StageRepo {
stageIndex, _ := getStageIndex(ctx, pkgType)
if nil == stageIndex {
return nil
}
stageIndex.reposOnce.Do(func() {
stageIndex.reposByURL = make(map[string]*StageRepo, len(stageIndex.Repos))
for _, r := range stageIndex.Repos {
stageIndex.reposByURL[r.URL] = r
}
})
return stageIndex.reposByURL[url]
}
func isBazaarOnline() (ret bool) {
// Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
if !ret {
util.PushErrMsg(util.Langs[util.Lang][24], 5000)
}
return
}
// bazaarStats 集市包统计信息
type bazaarStats struct {
Downloads int `json:"downloads"` // 下载次数
}
// cachedBazaarStats 缓存集市包统计信息
var cachedBazaarStats = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6)
// getBazaarStatsFromCache 仅从缓存获取集市包统计信息,过期或无缓存时返回 nil
func getBazaarStatsFromCache() (ret map[string]*bazaarStats) {
if val, found := cachedBazaarStats.Get("index"); found {
ret = val.(map[string]*bazaarStats)
}
return
}
// getBazaarStats 获取集市包统计信息
func getBazaarStats(ctx context.Context) map[string]*bazaarStats {
if cached := getBazaarStatsFromCache(); nil != cached {
return cached
}
v, _, _ := bazaarStatsFlight.Do("bazaarStats", func() (interface{}, error) {
return getBazaarStats0(ctx), nil
})
return v.(map[string]*bazaarStats)
}
func getBazaarStats0(ctx context.Context) map[string]*bazaarStats {
var result map[string]*bazaarStats
request := httpclient.NewBrowserRequest()
u := util.BazaarStatServer + "/bazaar/index.json"
resp, reqErr := request.SetContext(ctx).SetSuccessResult(&result).Get(u)
if nil != reqErr {
logging.LogErrorf("get bazaar stats [%s] failed: %s", u, reqErr)
return result
}
if 200 != resp.StatusCode {
logging.LogErrorf("get bazaar stats [%s] failed: %d", u, resp.StatusCode)
return result
}
cachedBazaarStats.SetDefault("index", result)
return result
}