Skip to content

Commit 97d2a7d

Browse files
committed
Add OTEL plumbing and harden pending read paths
1 parent 88d9031 commit 97d2a7d

30 files changed

Lines changed: 2950 additions & 313 deletions

cmd/wl/cmd_browse.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,18 @@ func runBrowseRemote(stdout, _ io.Writer, cfg *federation.Config, filter commons
206206

207207
func newBrowseClientConfig(cfg *federation.Config, db commons.DB) sdk.ClientConfig {
208208
clientCfg := sdk.ClientConfig{
209-
DB: db,
210-
RigHandle: cfg.RigHandle,
211-
Mode: cfg.ResolveMode(),
209+
DB: db,
210+
RigHandle: cfg.RigHandle,
211+
Mode: cfg.ResolveMode(),
212+
BestEffortPendingReads: true,
212213
}
213214
if cfg.ResolveMode() == federation.ModePR {
214215
clientCfg.LoadPendingItem = pendingItemLoaderCallback(cfg)
216+
clientCfg.LoadPendingItemContext = pendingItemLoaderContextCallback(cfg)
215217
clientCfg.LoadPendingDetail = pendingDetailLoaderCallback(cfg)
218+
clientCfg.LoadPendingDetailContext = pendingDetailLoaderContextCallback(cfg)
216219
clientCfg.ListPendingItems = listPendingItemsFromPRs(cfg)
220+
clientCfg.ListPendingItemsContext = listPendingItemsFromPRsContext(cfg)
217221
}
218222
return clientCfg
219223
}

cmd/wl/cmd_review.go

Lines changed: 224 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"io"
@@ -13,11 +14,13 @@ import (
1314

1415
"github.com/gastownhall/wasteland/internal/backend"
1516
"github.com/gastownhall/wasteland/internal/commons"
17+
"github.com/gastownhall/wasteland/internal/ctxutil"
1618
"github.com/gastownhall/wasteland/internal/federation"
1719
"github.com/gastownhall/wasteland/internal/remote"
1820
"github.com/gastownhall/wasteland/internal/sdk"
1921
"github.com/gastownhall/wasteland/internal/style"
2022
"github.com/spf13/cobra"
23+
"golang.org/x/sync/singleflight"
2124
)
2225

2326
type doltHubPRProvider interface {
@@ -27,6 +30,15 @@ type doltHubPRProvider interface {
2730
ClosePR(upstreamOrg, db, prID string) error
2831
}
2932

33+
func bindDoltHubPRProviderContext(ctx context.Context, provider doltHubPRProvider) doltHubPRProvider {
34+
if withCtx, ok := provider.(interface {
35+
WithContext(context.Context) *remote.DoltHubProvider
36+
}); ok {
37+
return withCtx.WithContext(ctx)
38+
}
39+
return provider
40+
}
41+
3042
var (
3143
pushBranchToRemoteForce = commons.PushBranchToRemoteForce
3244
newGitHubPRClientFromPath = func(ghPath string) GitHubPRClient {
@@ -470,7 +482,11 @@ func createGitHubPR(client GitHubPRClient, upstreamRepo, forkOrg, forkDB, wlBran
470482
// findExistingPR checks for an open PR on upstream with the given head ref.
471483
// Returns the PR URL and number, or empty strings if none found.
472484
func findExistingPR(ghPath, upstreamRepo, head string) (url, number string) {
473-
cmd := exec.Command(ghPath, "pr", "list", "--repo", upstreamRepo, "--head", head, "--state", "open", "--json", "number,url")
485+
return findExistingPRContext(context.Background(), ghPath, upstreamRepo, head)
486+
}
487+
488+
func findExistingPRContext(ctx context.Context, ghPath, upstreamRepo, head string) (url, number string) {
489+
cmd := exec.CommandContext(ctx, ghPath, "pr", "list", "--repo", upstreamRepo, "--head", head, "--state", "open", "--json", "number,url")
474490
out, err := cmd.CombinedOutput()
475491
if err != nil {
476492
return "", ""
@@ -761,15 +777,20 @@ func createPRForBranchRemote(cfg *federation.Config, cdb commons.DB, branch stri
761777
// checkPRForBranch checks if an upstream PR already exists for the given branch.
762778
// Returns the PR URL or empty string. Best-effort: returns "" on any error.
763779
func checkPRForBranch(cfg *federation.Config, branch string) string {
780+
return checkPRForBranchContext(context.Background(), cfg, branch)
781+
}
782+
783+
// checkPRForBranchContext checks if an upstream PR already exists for the given branch.
784+
// Returns the PR URL or empty string. Best-effort: returns "" on any error.
785+
func checkPRForBranchContext(ctx context.Context, cfg *federation.Config, branch string) string {
764786
switch cfg.ResolveProviderType() {
765787
case "github":
766788
ghPath, err := exec.LookPath("gh")
767789
if err != nil {
768790
return ""
769791
}
770-
client := newGHClient(ghPath)
771792
head := cfg.ForkOrg + ":" + branch
772-
url, _ := client.FindPR(cfg.Upstream, head)
793+
url, _ := findExistingPRContext(ctx, ghPath, cfg.Upstream, head)
773794
return url
774795
case "dolthub":
775796
token := os.Getenv("DOLTHUB_TOKEN")
@@ -780,7 +801,7 @@ func checkPRForBranch(cfg *federation.Config, branch string) string {
780801
if err != nil {
781802
return ""
782803
}
783-
provider := newDoltHubPRProvider(token)
804+
provider := bindDoltHubPRProviderContext(ctx, newDoltHubPRProvider(token))
784805
url, _ := provider.FindPR(upstreamOrg, db, cfg.ForkOrg, branch)
785806
return url
786807
default:
@@ -842,6 +863,78 @@ func listPendingItemsFromPRs(cfg *federation.Config) func() (map[string][]sdk.Pe
842863
}
843864
}
844865

866+
// listPendingItemsFromPRsContext returns a callback that lists wanted IDs with open
867+
// upstream PRs using request-scoped context. Returns nil if the provider type
868+
// does not support PR listing.
869+
func listPendingItemsFromPRsContext(cfg *federation.Config) func(context.Context) (map[string][]sdk.PendingItem, error) {
870+
switch cfg.ResolveProviderType() {
871+
case "dolthub":
872+
return dolthubListPendingItemsContext(cfg)
873+
case "github":
874+
ghPath, err := exec.LookPath("gh")
875+
if err != nil {
876+
return nil
877+
}
878+
return ghListPendingItemsContext(ghPath, cfg.Upstream)
879+
default:
880+
return nil
881+
}
882+
}
883+
884+
type pendingItemsFetcher func(context.Context) (map[string][]sdk.PendingItem, error)
885+
886+
type pendingItemsTTLCache struct {
887+
mu sync.RWMutex
888+
cached map[string][]sdk.PendingItem
889+
cachedAt time.Time
890+
cacheTTL time.Duration
891+
group singleflight.Group
892+
fetch pendingItemsFetcher
893+
}
894+
895+
func newPendingItemsTTLCache(cacheTTL time.Duration, fetch pendingItemsFetcher) func(context.Context) (map[string][]sdk.PendingItem, error) {
896+
cache := &pendingItemsTTLCache{
897+
cacheTTL: cacheTTL,
898+
fetch: fetch,
899+
}
900+
return cache.GetContext
901+
}
902+
903+
func (c *pendingItemsTTLCache) GetContext(ctx context.Context) (map[string][]sdk.PendingItem, error) {
904+
c.mu.RLock()
905+
cached := c.cached
906+
fresh := cached != nil && time.Since(c.cachedAt) < c.cacheTTL
907+
c.mu.RUnlock()
908+
if fresh {
909+
return cached, nil
910+
}
911+
912+
resultCh := c.group.DoChan("refresh", func() (any, error) {
913+
sharedCtx, cancel := ctxutil.Detached(ctx, pendingRefreshTimeout)
914+
defer cancel()
915+
items, err := c.fetch(sharedCtx)
916+
if err != nil {
917+
return nil, err
918+
}
919+
c.mu.Lock()
920+
c.cached = items
921+
c.cachedAt = time.Now()
922+
c.mu.Unlock()
923+
return items, nil
924+
})
925+
926+
select {
927+
case result := <-resultCh:
928+
if result.Err != nil {
929+
return nil, result.Err
930+
}
931+
items, _ := result.Val.(map[string][]sdk.PendingItem)
932+
return items, nil
933+
case <-ctx.Done():
934+
return nil, ctx.Err()
935+
}
936+
}
937+
845938
func dolthubListPendingItems(cfg *federation.Config) func() (map[string][]sdk.PendingItem, error) {
846939
token := commons.DoltHubToken()
847940
if token == "" {
@@ -961,10 +1054,109 @@ func ghListPendingItems(ghPath, upstreamRepo string) func() (map[string][]sdk.Pe
9611054
}
9621055
}
9631056

1057+
func dolthubListPendingItemsContext(cfg *federation.Config) func(context.Context) (map[string][]sdk.PendingItem, error) {
1058+
token := commons.DoltHubToken()
1059+
if token == "" {
1060+
return nil
1061+
}
1062+
upstreamOrg, db, err := federation.ParseUpstream(cfg.Upstream)
1063+
if err != nil {
1064+
return nil
1065+
}
1066+
return newPendingItemsTTLCache(30*time.Second, func(ctx context.Context) (map[string][]sdk.PendingItem, error) {
1067+
states, err := remote.NewDoltHubProvider(token).WithContext(ctx).ListPendingWantedIDs(upstreamOrg, db)
1068+
if err != nil {
1069+
return nil, err
1070+
}
1071+
result := make(map[string][]sdk.PendingItem, len(states))
1072+
for id, pending := range states {
1073+
items := make([]sdk.PendingItem, len(pending))
1074+
for i, p := range pending {
1075+
items[i] = sdk.PendingItem{
1076+
RigHandle: p.RigHandle,
1077+
Status: p.Status,
1078+
ClaimedBy: p.ClaimedBy,
1079+
Branch: p.Branch,
1080+
BranchURL: p.BranchURL,
1081+
PRURL: p.PRURL,
1082+
ForkOwner: p.ForkOwner,
1083+
CompletedBy: p.CompletedBy,
1084+
Evidence: p.Evidence,
1085+
}
1086+
}
1087+
result[id] = items
1088+
}
1089+
return result, nil
1090+
})
1091+
}
1092+
1093+
func ghListPendingItemsContext(ghPath, upstreamRepo string) func(context.Context) (map[string][]sdk.PendingItem, error) {
1094+
return newPendingItemsTTLCache(30*time.Second, func(ctx context.Context) (map[string][]sdk.PendingItem, error) {
1095+
out, err := exec.CommandContext(ctx, ghPath, "api", "--paginate",
1096+
fmt.Sprintf("repos/%s/pulls?state=open&per_page=100", upstreamRepo),
1097+
).CombinedOutput()
1098+
if err != nil {
1099+
return nil, fmt.Errorf("listing GitHub PRs: %w", err)
1100+
}
1101+
var prs []struct {
1102+
Head struct {
1103+
Ref string `json:"ref"`
1104+
} `json:"head"`
1105+
Title string `json:"title"`
1106+
User struct {
1107+
Login string `json:"login"`
1108+
} `json:"user"`
1109+
}
1110+
if err := json.Unmarshal(out, &prs); err != nil {
1111+
return nil, fmt.Errorf("parsing GitHub PRs: %w", err)
1112+
}
1113+
ids := make(map[string][]sdk.PendingItem)
1114+
for _, pr := range prs {
1115+
var rigHandle, wantedID string
1116+
1117+
parts := strings.SplitN(pr.Head.Ref, "/", 3)
1118+
if len(parts) == 3 && parts[0] == "wl" {
1119+
rigHandle = parts[1]
1120+
wantedID = parts[2]
1121+
}
1122+
1123+
if wantedID == "" {
1124+
if m := remote.WantedIDPattern.FindString(pr.Head.Ref); m != "" {
1125+
wantedID = m
1126+
} else if m := remote.WantedIDPattern.FindString(pr.Title); m != "" {
1127+
wantedID = m
1128+
}
1129+
}
1130+
1131+
if wantedID == "" {
1132+
continue
1133+
}
1134+
if rigHandle == "" {
1135+
rigHandle = pr.User.Login
1136+
}
1137+
1138+
ids[wantedID] = append(ids[wantedID], sdk.PendingItem{
1139+
RigHandle: rigHandle,
1140+
})
1141+
}
1142+
return ids, nil
1143+
})
1144+
}
1145+
9641146
// pendingItemLoaderCallback returns a callback that can read branch-only
9651147
// pending item summaries from the correct DoltHub fork. Returns nil when the
9661148
// current config does not support fork-aware remote reads.
9671149
func pendingItemLoaderCallback(cfg *federation.Config) func(string, sdk.PendingItem) (*commons.WantedItem, error) {
1150+
callback := pendingItemLoaderContextCallback(cfg)
1151+
if callback == nil {
1152+
return nil
1153+
}
1154+
return func(wantedID string, pending sdk.PendingItem) (*commons.WantedItem, error) {
1155+
return callback(context.Background(), wantedID, pending)
1156+
}
1157+
}
1158+
1159+
func pendingItemLoaderContextCallback(cfg *federation.Config) func(context.Context, string, sdk.PendingItem) (*commons.WantedItem, error) {
9681160
if cfg.ResolveBackend() == federation.BackendLocal || cfg.ResolveProviderType() != "dolthub" {
9691161
return nil
9701162
}
@@ -974,23 +1166,40 @@ func pendingItemLoaderCallback(cfg *federation.Config) func(string, sdk.PendingI
9741166
return nil
9751167
}
9761168

977-
return pendingItemLoader(upstreamOrg, db, cfg.ResolveMode(), commons.DoltHubToken())
1169+
return pendingItemLoaderContext(upstreamOrg, db, cfg.ResolveMode(), commons.DoltHubToken())
9781170
}
9791171

9801172
func pendingItemLoader(upstreamOrg, db, mode, token string) func(string, sdk.PendingItem) (*commons.WantedItem, error) {
1173+
callback := pendingItemLoaderContext(upstreamOrg, db, mode, token)
9811174
return func(wantedID string, pending sdk.PendingItem) (*commons.WantedItem, error) {
1175+
return callback(context.Background(), wantedID, pending)
1176+
}
1177+
}
1178+
1179+
func pendingItemLoaderContext(upstreamOrg, db, mode, token string) func(context.Context, string, sdk.PendingItem) (*commons.WantedItem, error) {
1180+
return func(ctx context.Context, wantedID string, pending sdk.PendingItem) (*commons.WantedItem, error) {
9821181
if pending.ForkOwner == "" || pending.Branch == "" {
9831182
return nil, fmt.Errorf("pending item %q is missing fork owner or branch", wantedID)
9841183
}
9851184
forkDB := backend.NewRemoteDB(token, upstreamOrg, db, pending.ForkOwner, db, mode)
986-
return commons.QueryWantedDetailAsOf(forkDB, wantedID, pending.Branch)
1185+
return commons.QueryWantedDetailAsOf(forkDB.WithContext(ctx), wantedID, pending.Branch)
9871186
}
9881187
}
9891188

9901189
// pendingDetailLoaderCallback returns a callback that can read branch-only
9911190
// pending items from the correct DoltHub fork. Returns nil when the current
9921191
// config does not support fork-aware remote reads.
9931192
func pendingDetailLoaderCallback(cfg *federation.Config) func(string, sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
1193+
callback := pendingDetailLoaderContextCallback(cfg)
1194+
if callback == nil {
1195+
return nil
1196+
}
1197+
return func(wantedID string, pending sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
1198+
return callback(context.Background(), wantedID, pending)
1199+
}
1200+
}
1201+
1202+
func pendingDetailLoaderContextCallback(cfg *federation.Config) func(context.Context, string, sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
9941203
if cfg.ResolveBackend() == federation.BackendLocal || cfg.ResolveProviderType() != "dolthub" {
9951204
return nil
9961205
}
@@ -1000,16 +1209,23 @@ func pendingDetailLoaderCallback(cfg *federation.Config) func(string, sdk.Pendin
10001209
return nil
10011210
}
10021211

1003-
return pendingDetailLoader(upstreamOrg, db, cfg.ResolveMode(), commons.DoltHubToken())
1212+
return pendingDetailLoaderContext(upstreamOrg, db, cfg.ResolveMode(), commons.DoltHubToken())
10041213
}
10051214

10061215
func pendingDetailLoader(upstreamOrg, db, mode, token string) func(string, sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
1216+
callback := pendingDetailLoaderContext(upstreamOrg, db, mode, token)
10071217
return func(wantedID string, pending sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
1218+
return callback(context.Background(), wantedID, pending)
1219+
}
1220+
}
1221+
1222+
func pendingDetailLoaderContext(upstreamOrg, db, mode, token string) func(context.Context, string, sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
1223+
return func(ctx context.Context, wantedID string, pending sdk.PendingItem) (*commons.WantedItem, *commons.CompletionRecord, *commons.Stamp, error) {
10081224
if pending.ForkOwner == "" || pending.Branch == "" {
10091225
return nil, nil, nil, fmt.Errorf("pending item %q is missing fork owner or branch", wantedID)
10101226
}
10111227
forkDB := backend.NewRemoteDB(token, upstreamOrg, db, pending.ForkOwner, db, mode)
1012-
return commons.QueryFullDetailAsOf(forkDB, wantedID, pending.Branch)
1228+
return commons.QueryFullDetailAsOf(forkDB.WithContext(ctx), wantedID, pending.Branch)
10131229
}
10141230
}
10151231

0 commit comments

Comments
 (0)