diff --git a/internal/applicant/acme-user.go b/internal/applicant/acme-user.go index 3b74d5ca..daa7a4cf 100644 --- a/internal/applicant/acme-user.go +++ b/internal/applicant/acme-user.go @@ -9,6 +9,7 @@ import ( "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" + "golang.org/x/sync/singleflight" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/utils/certs" @@ -79,9 +80,21 @@ type acmeAccountRepository interface { Save(ca, email, key string, resource *registration.Resource) error } -func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { - // TODO: fix 潜在的并发问题 +var registerGroup singleflight.Group +func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { + resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { + return register(client, sslProviderConfig, user) + }) + + if err != nil { + return nil, err + } + + return resp.(*registration.Resource), nil +} + +func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { var reg *registration.Resource var err error switch sslProviderConfig.Provider { diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 5b77c95f..6be0eb12 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -7,12 +7,14 @@ import ( "os" "strconv" "strings" + "sync" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/lego" + "golang.org/x/time/rate" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/utils/slices" @@ -184,6 +186,20 @@ type proxyApplicant struct { options *applicantOptions } +var limiters sync.Map + +const ( + limitBurst = 300 + limitRate float64 = float64(1) / float64(36) +) + +func getLimiter(key string) *rate.Limiter { + limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300)) + return limiter.(*rate.Limiter) +} + func (d *proxyApplicant) Apply() (*ApplyCertResult, error) { + limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail)) + limiter.Wait(context.Background()) return apply(d.applicant, d.options) } diff --git a/internal/applicant/applicant_test.go b/internal/applicant/applicant_test.go new file mode 100644 index 00000000..352cea24 --- /dev/null +++ b/internal/applicant/applicant_test.go @@ -0,0 +1,44 @@ +package applicant + +import ( + "testing" + "time" + + "golang.org/x/time/rate" +) + +func TestRateLimit(t *testing.T) { + tests := []struct { + name string + burst int + rate rate.Limit + }{ + { + name: "test1", + burst: 300, + rate: rate.Limit(float64(1) / float64(20)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rl := rate.NewLimiter(tt.rate, tt.burst) + if rl.Burst() != tt.burst { + t.Errorf("Burst() = %v, want %v", rl.Burst(), tt.burst) + } + if rl.Limit() != tt.rate { + t.Errorf("Limit() = %v, want %v", rl.Limit(), tt.rate) + } + + t.Log("consume all tokens at once", rl.AllowN(time.Now(), tt.burst)) + + t.Log("consume more", rl.Allow()) + + time.Sleep(time.Second * 5) + t.Log("consume after 5 seconds", rl.Allow()) + + time.Sleep(time.Second * 20) + t.Log("consume after 20 seconds", rl.Allow()) + }) + } +} diff --git a/ui/src/domain/version.ts b/ui/src/domain/version.ts index d61d0e40..8955d15c 100644 --- a/ui/src/domain/version.ts +++ b/ui/src/domain/version.ts @@ -1 +1 @@ -export const version = "v0.3.0-alpha.8"; +export const version = "v0.3.0-alpha.9";