mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
perf: Use circuit breaker to fail open Malysis query requests (#196)
* perf: Use circuit breaker to fail open Malysis query requests * fix: Linter fixes * test: Add test to confirm grpc status error unwrapping
This commit is contained in:
@@ -8,13 +8,13 @@ require (
|
|||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/elazarl/goproxy v1.8.1
|
github.com/elazarl/goproxy v1.8.1
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/goccy/go-yaml v1.19.2
|
|
||||||
github.com/google/osv-scalibr v0.2.1
|
github.com/google/osv-scalibr v0.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.7
|
github.com/jedib0t/go-pretty/v6 v6.6.7
|
||||||
github.com/posthog/posthog-go v1.5.12
|
github.com/posthog/posthog-go v1.5.12
|
||||||
github.com/safedep/dry v0.0.0-20260331131405-bd4c66ef7083
|
github.com/safedep/dry v0.0.0-20260331131405-bd4c66ef7083
|
||||||
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12
|
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12
|
||||||
|
github.com/sony/gobreaker/v2 v2.4.0
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
@@ -42,6 +42,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/go-github/v74 v74.0.0 // indirect
|
github.com/google/go-github/v74 v74.0.0 // indirect
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12/go.mod h1:fyt+PACz6
|
|||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
|
||||||
|
github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
"github.com/safedep/pmg/config"
|
"github.com/safedep/pmg/config"
|
||||||
"github.com/safedep/pmg/internal/eventlog"
|
"github.com/safedep/pmg/internal/eventlog"
|
||||||
"github.com/safedep/pmg/proxy"
|
"github.com/safedep/pmg/proxy"
|
||||||
|
gobreaker "github.com/sony/gobreaker/v2"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// baseRegistryInterceptor provides common functionality for registry interceptors
|
// baseRegistryInterceptor provides common functionality for registry interceptors
|
||||||
@@ -21,6 +24,25 @@ type baseRegistryInterceptor struct {
|
|||||||
cache AnalysisCache
|
cache AnalysisCache
|
||||||
statsCollector *AnalysisStatsCollector
|
statsCollector *AnalysisStatsCollector
|
||||||
confirmationChan chan *ConfirmationRequest
|
confirmationChan chan *ConfirmationRequest
|
||||||
|
circuitBreaker *gobreaker.CircuitBreaker[*analyzer.PackageVersionAnalysisResult]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnalyzerCircuitBreaker(name string) *gobreaker.CircuitBreaker[*analyzer.PackageVersionAnalysisResult] {
|
||||||
|
return newAnalyzerCircuitBreakerWithTimeout(name, 30*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnalyzerCircuitBreakerWithTimeout(name string, cooldown time.Duration) *gobreaker.CircuitBreaker[*analyzer.PackageVersionAnalysisResult] {
|
||||||
|
return gobreaker.NewCircuitBreaker[*analyzer.PackageVersionAnalysisResult](gobreaker.Settings{
|
||||||
|
Name: name,
|
||||||
|
MaxRequests: 1,
|
||||||
|
Timeout: cooldown,
|
||||||
|
ReadyToTrip: func(counts gobreaker.Counts) bool {
|
||||||
|
return counts.ConsecutiveFailures >= 3
|
||||||
|
},
|
||||||
|
OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
|
||||||
|
log.Infof("Circuit breaker %s: %s -> %s", name, from, to)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
|
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
|
||||||
@@ -87,10 +109,26 @@ func (b *baseRegistryInterceptor) analyzePackage(
|
|||||||
|
|
||||||
log.Debugf("[%s] Analyzing package %s@%s", ctx.RequestID, packageName, packageVersion)
|
log.Debugf("[%s] Analyzing package %s@%s", ctx.RequestID, packageName, packageVersion)
|
||||||
|
|
||||||
analysisCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
result, err := b.circuitBreaker.Execute(func() (*analyzer.PackageVersionAnalysisResult, error) {
|
||||||
defer cancel()
|
analysisCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
result, err := b.analyzer.Analyze(analysisCtx, pkgVersion)
|
res, err := b.analyzer.Analyze(analysisCtx, pkgVersion)
|
||||||
|
if err != nil {
|
||||||
|
// NotFound means the package is not in the analysis DB — this is expected
|
||||||
|
// and should not count as a circuit breaker failure.
|
||||||
|
// Since gRPC v1.75.0, status.FromError unwraps error chains via errors.As.
|
||||||
|
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||||
|
log.Debugf("[%s] Package %s@%s not found in analysis DB, allowing", ctx.RequestID, packageName, packageVersion)
|
||||||
|
return &analyzer.PackageVersionAnalysisResult{
|
||||||
|
PackageVersion: pkgVersion,
|
||||||
|
Action: analyzer.ActionAllow,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("analyzer failed: %w", err)
|
return nil, fmt.Errorf("analyzer failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package interceptors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
|
"github.com/safedep/pmg/analyzer"
|
||||||
|
"github.com/safedep/pmg/proxy"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockAnalyzer struct {
|
||||||
|
callCount int
|
||||||
|
err error
|
||||||
|
result *analyzer.PackageVersionAnalysisResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAnalyzer) Name() string { return "mock" }
|
||||||
|
|
||||||
|
func (m *mockAnalyzer) Analyze(_ context.Context, pv *packagev1.PackageVersion) (*analyzer.PackageVersionAnalysisResult, error) {
|
||||||
|
m.callCount++
|
||||||
|
if m.err != nil {
|
||||||
|
return nil, m.err
|
||||||
|
}
|
||||||
|
return m.result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestBaseInterceptor(a analyzer.PackageVersionAnalyzer) *baseRegistryInterceptor {
|
||||||
|
return &baseRegistryInterceptor{
|
||||||
|
analyzer: a,
|
||||||
|
cache: NewInMemoryAnalysisCache(),
|
||||||
|
statsCollector: NewAnalysisStatsCollector(),
|
||||||
|
confirmationChan: make(chan *ConfirmationRequest, 10),
|
||||||
|
circuitBreaker: newAnalyzerCircuitBreaker("test"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestRequestContext() *proxy.RequestContext {
|
||||||
|
parsedURL, _ := url.Parse("https://registry.npmjs.org/test/-/test-1.0.0.tgz")
|
||||||
|
return &proxy.RequestContext{
|
||||||
|
URL: parsedURL,
|
||||||
|
Method: "GET",
|
||||||
|
RequestID: "test-req",
|
||||||
|
StartTime: time.Now(),
|
||||||
|
Data: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_TripsAfterConsecutiveFailures(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: fmt.Errorf("rpc error: deadline exceeded")}
|
||||||
|
base := newTestBaseInterceptor(mock)
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
// First 3 calls should reach the analyzer (and fail)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 3, mock.callCount)
|
||||||
|
|
||||||
|
// 4th call should be blocked by circuit breaker without calling analyzer
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, 3, mock.callCount, "circuit breaker should prevent further analyzer calls")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_SuccessResetsFailureCount(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: fmt.Errorf("transient error")}
|
||||||
|
base := newTestBaseInterceptor(mock)
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
// 2 failures (not enough to trip)
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success resets the count
|
||||||
|
mock.err = nil
|
||||||
|
mock.result = &analyzer.PackageVersionAnalysisResult{Action: analyzer.ActionAllow}
|
||||||
|
result, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg2", "1.0.0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, analyzer.ActionAllow, result.Action)
|
||||||
|
|
||||||
|
// 2 more failures should not trip (count was reset)
|
||||||
|
mock.err = fmt.Errorf("transient error")
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg3", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 5, mock.callCount, "all calls should reach analyzer (breaker never tripped)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_RecoveryAfterCooldown(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: fmt.Errorf("rpc error: deadline exceeded")}
|
||||||
|
|
||||||
|
base := &baseRegistryInterceptor{
|
||||||
|
analyzer: mock,
|
||||||
|
cache: NewInMemoryAnalysisCache(),
|
||||||
|
statsCollector: NewAnalysisStatsCollector(),
|
||||||
|
confirmationChan: make(chan *ConfirmationRequest, 10),
|
||||||
|
// Use a very short cooldown for testing
|
||||||
|
circuitBreaker: newAnalyzerCircuitBreakerWithTimeout("test-recovery", 1*time.Second),
|
||||||
|
}
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
// Trip the breaker
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
_, _ = base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg", "1.0.0")
|
||||||
|
}
|
||||||
|
assert.Equal(t, 3, mock.callCount)
|
||||||
|
|
||||||
|
// Breaker is open — calls don't reach analyzer
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, 3, mock.callCount)
|
||||||
|
|
||||||
|
// Wait for cooldown, then the breaker enters half-open and allows a probe
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
|
||||||
|
mock.err = nil
|
||||||
|
mock.result = &analyzer.PackageVersionAnalysisResult{Action: analyzer.ActionAllow}
|
||||||
|
result, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "pkg4", "1.0.0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, analyzer.ActionAllow, result.Action)
|
||||||
|
assert.Equal(t, 4, mock.callCount, "probe request should reach analyzer")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_CacheBypassesBreaker(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: fmt.Errorf("rpc error: deadline exceeded")}
|
||||||
|
base := newTestBaseInterceptor(mock)
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
// Pre-populate cache
|
||||||
|
base.cache.Set(packagev1.Ecosystem_ECOSYSTEM_NPM.String(), "cached-pkg", "1.0.0", &analyzer.PackageVersionAnalysisResult{
|
||||||
|
Action: analyzer.ActionAllow,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Trip the breaker with other packages
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
_, _ = base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, fmt.Sprintf("fail-%d", i), "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached package should still be served even though breaker is open
|
||||||
|
result, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "cached-pkg", "1.0.0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, analyzer.ActionAllow, result.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_NotFoundDoesNotCountAsFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single wrapped gRPC error (analyzer layer)",
|
||||||
|
err: fmt.Errorf("failed to query package analysis: %w", status.Error(codes.NotFound, "package not found")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double wrapped gRPC error (interceptor + analyzer layers)",
|
||||||
|
err: fmt.Errorf("analyzer failed: %w", fmt.Errorf("failed to query package analysis: %w", status.Error(codes.NotFound, "package not found"))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: tt.err}
|
||||||
|
base := newTestBaseInterceptor(mock)
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
result, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, fmt.Sprintf("unknown-%d", i), "1.0.0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, analyzer.ActionAllow, result.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 5, mock.callCount, "all calls should reach analyzer (breaker never tripped)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCircuitBreaker_NotFoundFollowedByRealFailures(t *testing.T) {
|
||||||
|
mock := &mockAnalyzer{err: fmt.Errorf("failed to query package analysis: %w", status.Error(codes.NotFound, "not found"))}
|
||||||
|
base := newTestBaseInterceptor(mock)
|
||||||
|
ctx := newTestRequestContext()
|
||||||
|
|
||||||
|
// 3 NotFound calls — should NOT trip the breaker
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
result, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, fmt.Sprintf("notfound-%d", i), "1.0.0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, analyzer.ActionAllow, result.Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to real failures
|
||||||
|
mock.err = fmt.Errorf("rpc error: deadline exceeded")
|
||||||
|
|
||||||
|
// 3 real failures should trip the breaker
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, fmt.Sprintf("fail-%d", i), "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7th call should be blocked by breaker
|
||||||
|
_, err := base.analyzePackage(ctx, packagev1.Ecosystem_ECOSYSTEM_NPM, "blocked", "1.0.0")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, 6, mock.callCount, "breaker should prevent 7th call from reaching analyzer")
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ func NewNpmRegistryInterceptor(
|
|||||||
cache: cache,
|
cache: cache,
|
||||||
statsCollector: statsCollector,
|
statsCollector: statsCollector,
|
||||||
confirmationChan: confirmationChan,
|
confirmationChan: confirmationChan,
|
||||||
|
circuitBreaker: newAnalyzerCircuitBreaker("malysis-analyzer-npm"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ func (i *NpmRegistryInterceptor) HandleRequest(ctx *proxy.RequestContext) (*prox
|
|||||||
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
return &proxy.InterceptorResponse{Action: proxy.ActionAllow}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := i.baseRegistryInterceptor.analyzePackage(
|
result, err := i.analyzePackage(
|
||||||
ctx,
|
ctx,
|
||||||
packagev1.Ecosystem_ECOSYSTEM_NPM,
|
packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||||
pkgInfo.GetName(),
|
pkgInfo.GetName(),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func NewPypiRegistryInterceptor(
|
|||||||
cache: cache,
|
cache: cache,
|
||||||
statsCollector: statsCollector,
|
statsCollector: statsCollector,
|
||||||
confirmationChan: confirmationChan,
|
confirmationChan: confirmationChan,
|
||||||
|
circuitBreaker: newAnalyzerCircuitBreaker("malysis-analyzer-pypi"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user