Skip to content

Commit bba21b6

Browse files
authored
Merge pull request #154 from Infisical/tre/eng-4704-investigate-global-latency
improve: reduce calls to check for update, do local jwt refresh check
2 parents 31e4b11 + e0c919c commit bba21b6

11 files changed

Lines changed: 595 additions & 39 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Mark latest release as urgent
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: write
8+
9+
jobs:
10+
mark-urgent:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Mark latest release as urgent
14+
env:
15+
GH_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
16+
run: |
17+
LATEST_TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name')
18+
CURRENT_BODY=$(gh api repos/${{ github.repository }}/releases/latest --jq '.body')
19+
gh release edit "$LATEST_TAG" --repo "${{ github.repository }}" --notes "${CURRENT_BODY}
20+
<!-- #urgent -->"

e2e/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ require (
125125
github.com/godbus/dbus/v5 v5.1.0 // indirect
126126
github.com/gofrs/flock v0.12.1 // indirect
127127
github.com/gogo/protobuf v1.3.2 // indirect
128-
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
128+
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
129129
github.com/golang/glog v1.2.5 // indirect
130130
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
131131
github.com/golang/protobuf v1.5.4 // indirect

e2e/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
379379
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
380380
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
381381
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
382-
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
383-
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
382+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
383+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
384384
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
385385
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
386386
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/fatih/semgroup v1.2.0
1515
github.com/gitleaks/go-gitdiff v0.9.1
1616
github.com/go-mysql-org/go-mysql v1.13.0
17+
github.com/golang-jwt/jwt/v5 v5.3.1
1718
github.com/google/uuid v1.6.0
1819
github.com/h2non/filetype v1.1.3
1920
github.com/infisical/go-sdk v0.6.8

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
226226
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
227227
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
228228
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
229+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
230+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
229231
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
230232
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
231233
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=

packages/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func RootCmdStdoutWriter() io.Writer {
6464
// Execute adds all child commands to the root command and sets flags appropriately.
6565
// This is called by main.main(). It only needs to happen once to the RootCmd.
6666
func Execute() {
67+
defer util.WaitForUpdateCheck()
6768
err := RootCmd.Execute()
6869
if err != nil {
6970
os.Exit(1)

packages/util/check-for-update.go

Lines changed: 165 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,83 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"io/ioutil"
98
"net/http"
109
"os"
1110
"os/exec"
11+
"path/filepath"
1212
"runtime"
1313
"strings"
14+
"sync"
1415
"time"
1516

1617
"github.com/fatih/color"
1718
"github.com/rs/zerolog/log"
1819
)
1920

20-
func CheckForUpdate() {
21-
CheckForUpdateWithWriter(os.Stderr)
21+
var githubHTTPClient = &http.Client{Timeout: 8 * time.Second}
22+
23+
var updateCheckWg sync.WaitGroup
24+
25+
const updateCheckCacheTTL = 24 * time.Hour
26+
const urgentUpdateCheckCacheTTL = 5 * time.Minute
27+
28+
type UpdateCheckCache struct {
29+
LastCheckTime time.Time `json:"lastCheckTime"`
30+
LatestVersion string `json:"latestVersion"`
31+
LatestVersionPublishedAt time.Time `json:"latestVersionPublishedAt"`
32+
CurrentVersionPublishedAt time.Time `json:"currentVersionPublishedAt"`
33+
IsUrgent bool `json:"isUrgent"`
34+
CurrentVersionAtCheck string `json:"currentVersionAtCheck"`
2235
}
2336

2437
func CheckForUpdateWithWriter(w io.Writer) {
2538
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
2639
return
2740
}
28-
latestVersion, _, isUrgent, err := getLatestTag("Infisical", "cli")
29-
if err != nil {
30-
log.Debug().Err(err)
31-
// do nothing and continue
32-
return
41+
42+
cache := readUpdateCheckCache()
43+
44+
displayCachedUpdateNotice(w, cache)
45+
46+
if !isCacheFresh(cache) {
47+
updateCheckWg.Add(1)
48+
go func() {
49+
defer updateCheckWg.Done()
50+
performUpdateCheckInBackground()
51+
}()
3352
}
53+
}
3454

35-
if latestVersion == CLI_VERSION {
36-
return
55+
// WaitForUpdateCheck blocks until the background update check goroutine completes.
56+
// Call this before program exit to ensure the cache gets written.
57+
func WaitForUpdateCheck() {
58+
updateCheckWg.Wait()
59+
}
60+
61+
// isCacheFresh returns true if the cache is fresh enough to skip a network check.
62+
func isCacheFresh(cache *UpdateCheckCache) bool {
63+
if cache == nil || cache.LatestVersion == "" || cache.CurrentVersionAtCheck != CLI_VERSION {
64+
return false
3765
}
66+
ttl := updateCheckCacheTTL
67+
if cache.IsUrgent {
68+
ttl = urgentUpdateCheckCacheTTL
69+
}
70+
return time.Since(cache.LastCheckTime) < ttl
71+
}
3872

39-
// Only prompt if the user's current version is at least 48 hours old, unless urgent.
40-
// This avoids nagging users who recently updated.
41-
currentVersionPublishedAt, err := getReleasePublishedAt("Infisical", "cli", CLI_VERSION)
42-
if err == nil && !isUrgent && time.Since(currentVersionPublishedAt).Hours() < 48 {
73+
// displayCachedUpdateNotice prints an update notification from cached data.
74+
func displayCachedUpdateNotice(w io.Writer, cache *UpdateCheckCache) {
75+
if cache == nil || cache.LatestVersion == "" || cache.LatestVersion == CLI_VERSION {
76+
return
77+
}
78+
// Don't show stale notifications after the user has upgraded.
79+
if cache.CurrentVersionAtCheck != CLI_VERSION {
80+
return
81+
}
82+
// Unless urgent, skip notification if the current version is less than 48h old.
83+
if !cache.IsUrgent && !cache.CurrentVersionPublishedAt.IsZero() &&
84+
time.Since(cache.CurrentVersionPublishedAt).Hours() < 48 {
4385
return
4486
}
4587

@@ -51,19 +93,122 @@ func CheckForUpdateWithWriter(w io.Writer) {
5193
yellow("A new release of infisical is available:"),
5294
blue(CLI_VERSION),
5395
black("->"),
54-
blue(latestVersion),
96+
blue(cache.LatestVersion),
5597
)
5698

5799
fmt.Fprintln(w, msg)
58100

59101
updateInstructions := GetUpdateInstructions()
60-
61102
if updateInstructions != "" {
62-
msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())
103+
msg = fmt.Sprintf("\n%s\n", updateInstructions)
63104
fmt.Fprintln(w, msg)
64105
}
65106
}
66107

108+
// performUpdateCheckInBackground fetches update info from GitHub and writes to cache.
109+
// It is designed to be called as a fire-and-forget goroutine.
110+
func performUpdateCheckInBackground() {
111+
latestVersion, latestPublishedAt, isUrgent, err := getLatestTag("Infisical", "cli")
112+
if err != nil {
113+
log.Debug().Err(err).Msg("background update check: failed to get latest tag")
114+
return
115+
}
116+
117+
cache := &UpdateCheckCache{
118+
LastCheckTime: time.Now(),
119+
LatestVersion: latestVersion,
120+
LatestVersionPublishedAt: latestPublishedAt,
121+
IsUrgent: isUrgent,
122+
CurrentVersionAtCheck: CLI_VERSION,
123+
}
124+
125+
// If versions differ, fetch the publish date for the current version (for 48h grace).
126+
if latestVersion != CLI_VERSION {
127+
currentPublishedAt, err := getReleasePublishedAt("Infisical", "cli", CLI_VERSION)
128+
if err != nil {
129+
log.Debug().Err(err).Msg("background update check: failed to get current version publish date")
130+
// Non-fatal — we just won't have the 48h grace period data.
131+
} else {
132+
cache.CurrentVersionPublishedAt = currentPublishedAt
133+
}
134+
}
135+
136+
if err := writeUpdateCheckCache(cache); err != nil {
137+
log.Debug().Err(err).Msg("background update check: failed to write cache")
138+
}
139+
}
140+
141+
// getUpdateCheckCachePath returns the path to ~/.infisical/update-check.json.
142+
func getUpdateCheckCachePath() (string, error) {
143+
homeDir, err := GetHomeDir()
144+
if err != nil {
145+
return "", err
146+
}
147+
return filepath.Join(homeDir, CONFIG_FOLDER_NAME, UPDATE_CHECK_CACHE_FILE_NAME), nil
148+
}
149+
150+
// readUpdateCheckCache reads and unmarshals the cache file. Returns nil on any error (cache miss).
151+
func readUpdateCheckCache() *UpdateCheckCache {
152+
path, err := getUpdateCheckCachePath()
153+
if err != nil {
154+
return nil
155+
}
156+
157+
data, err := os.ReadFile(path)
158+
if err != nil {
159+
return nil
160+
}
161+
162+
var cache UpdateCheckCache
163+
if err := json.Unmarshal(data, &cache); err != nil {
164+
return nil
165+
}
166+
167+
return &cache
168+
}
169+
170+
// writeUpdateCheckCache atomically writes the cache file using a temp file + rename.
171+
func writeUpdateCheckCache(cache *UpdateCheckCache) error {
172+
path, err := getUpdateCheckCachePath()
173+
if err != nil {
174+
return err
175+
}
176+
177+
dir := filepath.Dir(path)
178+
if err := os.MkdirAll(dir, 0700); err != nil {
179+
return fmt.Errorf("failed to create cache directory: %w", err)
180+
}
181+
182+
data, err := json.Marshal(cache)
183+
if err != nil {
184+
return fmt.Errorf("failed to marshal cache: %w", err)
185+
}
186+
187+
tmpFile, err := os.CreateTemp(dir, "update-check-*.json.tmp")
188+
if err != nil {
189+
return fmt.Errorf("failed to create temp file: %w", err)
190+
}
191+
tmpPath := tmpFile.Name()
192+
193+
if _, err := tmpFile.Write(data); err != nil {
194+
tmpFile.Close()
195+
os.Remove(tmpPath)
196+
return fmt.Errorf("failed to write temp file: %w", err)
197+
}
198+
199+
if err := tmpFile.Close(); err != nil {
200+
os.Remove(tmpPath)
201+
return fmt.Errorf("failed to close temp file: %w", err)
202+
}
203+
204+
if err := os.Rename(tmpPath, path); err != nil {
205+
os.Remove(tmpPath)
206+
return fmt.Errorf("failed to rename temp file: %w", err)
207+
}
208+
209+
return nil
210+
}
211+
67212
func DisplayAptInstallationChangeBanner(isSilent bool) {
68213
DisplayAptInstallationChangeBannerWithWriter(isSilent, os.Stderr)
69214
}
@@ -89,7 +234,7 @@ func DisplayAptInstallationChangeBannerWithWriter(isSilent bool, w io.Writer) {
89234

90235
func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, error) {
91236
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName)
92-
resp, err := http.Get(url)
237+
resp, err := githubHTTPClient.Get(url)
93238
if err != nil {
94239
return "", time.Time{}, false, err
95240
}
@@ -132,7 +277,7 @@ func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, e
132277
func getReleasePublishedAt(repoOwner string, repoName string, version string) (time.Time, error) {
133278
tag := "v" + version
134279
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", repoOwner, repoName, tag)
135-
resp, err := http.Get(url)
280+
resp, err := githubHTTPClient.Get(url)
136281
if err != nil {
137282
return time.Time{}, err
138283
}
@@ -218,7 +363,7 @@ func IsRunningInDocker() bool {
218363
return true
219364
}
220365

221-
cgroup, err := ioutil.ReadFile("/proc/self/cgroup")
366+
cgroup, err := os.ReadFile("/proc/self/cgroup")
222367
if err != nil {
223368
return false
224369
}

0 commit comments

Comments
 (0)