@@ -5,6 +5,13 @@ import (
55 "encoding/json"
66 "errors"
77 "fmt"
8+ "net"
9+ "net/http"
10+ "strings"
11+ "time"
12+
13+ "github.com/bartventer/httpcache"
14+ _ "github.com/cirruslabs/cirrus-cli/internal/evaluator/lrucache"
815 "github.com/cirruslabs/cirrus-cli/internal/version"
916 "github.com/cirruslabs/cirrus-cli/pkg/api"
1017 "github.com/cirruslabs/cirrus-cli/pkg/larker"
@@ -14,6 +21,7 @@ import (
1421 "github.com/cirruslabs/cirrus-cli/pkg/larker/fs/memory"
1522 "github.com/cirruslabs/cirrus-cli/pkg/parser"
1623 "github.com/cirruslabs/cirrus-cli/pkg/parser/parsererror"
24+ "github.com/jellydator/ttlcache/v3"
1725 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
1826 "go.opentelemetry.io/otel/metric/noop"
1927 "google.golang.org/grpc"
@@ -23,8 +31,6 @@ import (
2331 "google.golang.org/protobuf/reflect/protodesc"
2432 "google.golang.org/protobuf/reflect/protoreflect"
2533 "google.golang.org/protobuf/types/known/structpb"
26- "net"
27- "strings"
2834)
2935
3036const pathYAML = ".cirrus.yml"
@@ -33,6 +39,9 @@ const pathStarlark = ".cirrus.star"
3339var ErrNoFS = errors .New ("no filesystem available" )
3440
3541type ConfigurationEvaluatorServiceServer struct {
42+ perTenantCachingHTTPClients * ttlcache.Cache [string , * http.Client ]
43+ roundTripperForTests http.RoundTripper
44+
3645 // must be embedded to have forward compatible implementations
3746 api.UnimplementedCirrusConfigurationEvaluatorServiceServer
3847}
@@ -54,15 +63,26 @@ func addVersion(
5463 return handler (ctx , req )
5564}
5665
57- func Serve (ctx context.Context , lis net.Listener ) error {
66+ func Serve (ctx context.Context , lis net.Listener , opts ... Option ) error {
5867 server := grpc .NewServer (
5968 grpc .UnaryInterceptor (addVersion ),
6069 grpc .StatsHandler (otelgrpc .NewServerHandler (
6170 otelgrpc .WithMeterProvider (noop .NewMeterProvider ()),
6271 )),
6372 )
6473
65- api .RegisterCirrusConfigurationEvaluatorServiceServer (server , & ConfigurationEvaluatorServiceServer {})
74+ r := & ConfigurationEvaluatorServiceServer {
75+ perTenantCachingHTTPClients : ttlcache.New [string , * http.Client ](
76+ ttlcache.WithTTL [string , * http.Client ](24 * time .Hour ),
77+ ),
78+ }
79+
80+ // Apply opts
81+ for _ , opt := range opts {
82+ opt (r )
83+ }
84+
85+ api .RegisterCirrusConfigurationEvaluatorServiceServer (server , r )
6686
6787 errChan := make (chan error )
6888
@@ -97,7 +117,15 @@ func (r *ConfigurationEvaluatorServiceServer) EvaluateConfig(
97117 yamlConfigs = append (yamlConfigs , request .YamlConfig )
98118 }
99119
100- fs , err := convertFS (request .Fs )
120+ var httpClient * http.Client
121+
122+ if githubFS := request .GetFs ().GetGithub (); githubFS != nil {
123+ if httpCache := githubFS .GetHttpCache (); httpCache != nil {
124+ httpClient = r .cachingHTTPClient (httpCache .GetTenant (), httpCache .GetSize ())
125+ }
126+ }
127+
128+ fs , err := convertFS (request .Fs , httpClient )
101129 if err != nil {
102130 return nil , status .Errorf (codes .Internal , "failed to initialize file system: %v" , err )
103131 }
@@ -109,6 +137,7 @@ func (r *ConfigurationEvaluatorServiceServer) EvaluateConfig(
109137 larker .WithFileSystem (fs ),
110138 larker .WithEnvironment (request .Environment ),
111139 larker .WithAffectedFiles (request .AffectedFiles ),
140+ larker .WithHTTPClient (httpClient ),
112141 )
113142
114143 lrkResult , err := lrk .MainOptional (ctx , request .StarlarkConfig )
@@ -207,14 +236,23 @@ func (r *ConfigurationEvaluatorServiceServer) EvaluateFunction(
207236 ctx context.Context ,
208237 request * api.EvaluateFunctionRequest ,
209238) (* api.EvaluateFunctionResponse , error ) {
210- fs , err := convertFS (request .Fs )
239+ var httpClient * http.Client
240+
241+ if githubFS := request .GetFs ().GetGithub (); githubFS != nil {
242+ if httpCache := githubFS .GetHttpCache (); httpCache != nil {
243+ httpClient = r .cachingHTTPClient (httpCache .GetTenant (), httpCache .GetSize ())
244+ }
245+ }
246+
247+ fs , err := convertFS (request .Fs , httpClient )
211248 if err != nil {
212249 return nil , status .Errorf (codes .Internal , "failed to initialize file system: %v" , err )
213250 }
214251
215252 lrk := larker .New (
216253 larker .WithFileSystem (fs ),
217254 larker .WithEnvironment (request .Environment ),
255+ larker .WithHTTPClient (httpClient ),
218256 )
219257
220258 // Run Starlark hook
@@ -281,7 +319,7 @@ func TransformAdditionalInstances(
281319 return additionalInstances , nil
282320}
283321
284- func convertFS (apiFS * api.FileSystem ) (fs fs.FileSystem , err error ) {
322+ func convertFS (apiFS * api.FileSystem , httpClient * http. Client ) (fs fs.FileSystem , err error ) {
285323 fs = failing .New (ErrNoFS )
286324
287325 if apiFS == nil {
@@ -292,8 +330,40 @@ func convertFS(apiFS *api.FileSystem) (fs fs.FileSystem, err error) {
292330 case * api.FileSystem_Memory_ :
293331 fs , err = memory .New (impl .Memory .FilesContents )
294332 case * api.FileSystem_Github_ :
295- fs , err = github .New (impl .Github .Owner , impl .Github .Repo , impl .Github .Reference , impl .Github .Token )
333+ fs , err = github .New (impl .Github .Owner , impl .Github .Repo , impl .Github .Reference , impl .Github .Token ,
334+ httpClient )
296335 }
297336
298337 return fs , err
299338}
339+
340+ func (r * ConfigurationEvaluatorServiceServer ) cachingHTTPClient (tenant string , size int32 ) * http.Client {
341+ if tenant == "" || size == 0 {
342+ return nil
343+ }
344+
345+ httpClient , _ := r .perTenantCachingHTTPClients .GetOrSetFunc (tenant , func () * http.Client {
346+ dsn := fmt .Sprintf ("lrucache://?size=%d" , size )
347+
348+ httpClient := httpcache .NewClient (dsn , httpcache .WithUpstream (r .roundTripper ()))
349+
350+ // GitHub has a 10-second timeout for API requests
351+ httpClient .Timeout = 11 * time .Second
352+
353+ return httpClient
354+ })
355+
356+ return httpClient .Value ()
357+ }
358+
359+ func (r * ConfigurationEvaluatorServiceServer ) roundTripper () http.RoundTripper {
360+ if r .roundTripperForTests != nil {
361+ return r .roundTripperForTests
362+ }
363+
364+ return & http.Transport {
365+ MaxIdleConns : 1024 ,
366+ MaxIdleConnsPerHost : 1024 , // default is 2 which is too small and we mostly access the same host
367+ IdleConnTimeout : time .Minute , // let's put something big but not infinite like the default
368+ }
369+ }
0 commit comments