@@ -17,8 +17,10 @@ package handler
17
17
import (
18
18
"context"
19
19
"errors"
20
+ "fmt"
20
21
"net/http"
21
22
"os"
23
+ "sync"
22
24
"time"
23
25
24
26
"github.com/google/go-github/v72/github"
@@ -39,8 +41,110 @@ type FetchedConfig struct {
39
41
Path string
40
42
}
41
43
44
+ type ConfigCache struct {
45
+ mu sync.RWMutex
46
+ expiry time.Time
47
+ config * FetchedConfig
48
+ }
49
+
50
+ func (cc * ConfigCache ) GetOrUpdate (fn func () (* FetchedConfig , error )) (* FetchedConfig , bool , error ) {
51
+ const cacheUpdateTimeout = 30 * time .Second
52
+ const cacheExpiryDuration = 1 * time .Minute
53
+
54
+ // Works both when the cache has not expired, and when another process is currently updating the cache (block on RLock).
55
+ cached := func () * FetchedConfig {
56
+ cc .mu .RLock ()
57
+ defer cc .mu .RUnlock ()
58
+
59
+ if time .Now ().Before (cc .expiry ) {
60
+ return cc .config
61
+ }
62
+ return nil
63
+ }()
64
+ if cached != nil {
65
+ return cached , true , nil
66
+ }
67
+
68
+ // Update
69
+ cc .mu .Lock ()
70
+ defer cc .mu .Unlock ()
71
+
72
+ timeoutCtx , cancel := context .WithTimeout (context .Background (), cacheUpdateTimeout )
73
+ defer cancel ()
74
+
75
+ var value * FetchedConfig
76
+ var err error
77
+ var updateDone = make (chan struct {}, 1 )
78
+ go func () {
79
+ defer close (updateDone )
80
+ value , err = fn ()
81
+ }()
82
+
83
+ select {
84
+ case <- timeoutCtx .Done ():
85
+ return nil , false , errors .New ("cache update timed out" )
86
+ case <- updateDone :
87
+ if err != nil {
88
+ return nil , false , err
89
+ }
90
+ cc .config = value
91
+ cc .expiry = time .Now ().Add (cacheExpiryDuration )
92
+ return cc .config , false , nil
93
+ }
94
+ }
95
+
42
96
type ConfigFetcher struct {
43
- Loader * appconfig.Loader
97
+ sharedConfigCache ConfigCache
98
+
99
+ Options PullEvaluationOptions
100
+ Loader * appconfig.Loader
101
+ }
102
+
103
+ func (cf * ConfigFetcher ) configForSharedRepository (ctx context.Context , client * github.Client , owner string ) (* FetchedConfig , error ) {
104
+ r , _ , err := client .Repositories .Get (ctx , owner , * cf .Options .SharedRepository )
105
+ if err != nil {
106
+ return nil , fmt .Errorf ("failed to get repository %s/%s: %w" , owner , * cf .Options .SharedRepository , err )
107
+ }
108
+
109
+ ref := r .GetDefaultBranch ()
110
+ file , _ , _ , err := client .Repositories .GetContents (ctx , owner , * cf .Options .SharedRepository , * cf .Options .SharedPolicyPath , & github.RepositoryContentGetOptions {
111
+ Ref : ref ,
112
+ })
113
+ if err != nil {
114
+ return nil , fmt .Errorf ("failed to get file %s/%s@%s:%s: %w" , owner , * cf .Options .SharedRepository , ref , * cf .Options .SharedPolicyPath , err )
115
+ }
116
+
117
+ content , err := file .GetContent ()
118
+ if err != nil {
119
+ return nil , fmt .Errorf ("failed to get content of file %s/%s@%s:%s: %w" , owner , * cf .Options .SharedRepository , ref , * cf .Options .SharedPolicyPath , err )
120
+ }
121
+
122
+ var pc policy.Config
123
+ if err := yaml .UnmarshalStrict ([]byte (content ), & pc ); err != nil {
124
+ return nil , fmt .Errorf ("failed to parse content of file %s/%s@%s:%s: %w" , owner , * cf .Options .SharedRepository , ref , * cf .Options .SharedPolicyPath , err )
125
+ }
126
+
127
+ fc := & FetchedConfig {
128
+ Config : & pc ,
129
+ Source : fmt .Sprintf ("%s/%s@%s" , owner , * cf .Options .SharedRepository , ref ),
130
+ Path : * cf .Options .SharedPolicyPath ,
131
+ }
132
+ return fc , nil
133
+ }
134
+
135
+ func (cf * ConfigFetcher ) ConfigForSharedRepository (ctx context.Context , client * github.Client , owner string ) FetchedConfig {
136
+ ctx , span := tracing .Tracer .Start (ctx , "ConfigFetcher.ConfigForSharedRepository" )
137
+ defer span .End ()
138
+
139
+ config , cached , err := cf .sharedConfigCache .GetOrUpdate (func () (* FetchedConfig , error ) {
140
+ return cf .configForSharedRepository (ctx , client , owner )
141
+ })
142
+ span .SetAttributes (attribute .Bool ("cache.hit" , cached ))
143
+
144
+ if err != nil {
145
+ return FetchedConfig {LoadError : err }
146
+ }
147
+ return * config
44
148
}
45
149
46
150
func (cf * ConfigFetcher ) ConfigForRepositoryBranch (ctx context.Context , client * github.Client , owner , repository , branch string ) FetchedConfig {
@@ -52,6 +156,10 @@ func (cf *ConfigFetcher) ConfigForRepositoryBranch(ctx context.Context, client *
52
156
))
53
157
defer span .End ()
54
158
159
+ if cf .Options .ForceSharedPolicy {
160
+ return cf .ConfigForSharedRepository (ctx , client , owner )
161
+ }
162
+
55
163
retries := 0
56
164
delay := 1 * time .Second
57
165
for {
0 commit comments