SQS: ValidateQueueName в createTenantQueue, JWT требует exp, leader сбрасывает лидерство при длительном сбое refresh
This commit is contained in:
@@ -510,6 +510,12 @@ func (h *Handler) createTenantQueue(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonErr(w, http.StatusBadRequest, "name is required")
|
jsonErr(w, http.StatusBadRequest, "name is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Та же валидация имени, что и в SQS CreateQueue (иначе имена с ':'/'/'
|
||||||
|
// ломали бы ключ "{accessKey}:{name}").
|
||||||
|
if err := gosqs.ValidateQueueName(req.Name); err != nil {
|
||||||
|
jsonErr(w, http.StatusBadRequest, "invalid queue name")
|
||||||
|
return
|
||||||
|
}
|
||||||
key := t.AccessKey + ":" + req.Name
|
key := t.AccessKey + ":" + req.Name
|
||||||
models.SyncQueues.Lock()
|
models.SyncQueues.Lock()
|
||||||
if _, exists := models.SyncQueues.Queues[key]; exists {
|
if _, exists := models.SyncQueues.Queues[key]; exists {
|
||||||
|
|||||||
+4
-1
@@ -59,7 +59,10 @@ func ParseJWTClaims(token string) (*JWTClaims, error) {
|
|||||||
if claims.Sub == "" {
|
if claims.Sub == "" {
|
||||||
return nil, fmt.Errorf("JWT missing 'sub' claim")
|
return nil, fmt.Errorf("JWT missing 'sub' claim")
|
||||||
}
|
}
|
||||||
if claims.Exp > 0 && claims.Exp < time.Now().Unix() {
|
if claims.Exp == 0 {
|
||||||
|
return nil, fmt.Errorf("JWT missing 'exp' claim")
|
||||||
|
}
|
||||||
|
if claims.Exp < time.Now().Unix() {
|
||||||
return nil, fmt.Errorf("JWT token expired")
|
return nil, fmt.Errorf("JWT token expired")
|
||||||
}
|
}
|
||||||
return &claims, nil
|
return &claims, nil
|
||||||
|
|||||||
+30
-5
@@ -33,16 +33,21 @@ type Elector struct {
|
|||||||
isLeader bool
|
isLeader bool
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
stopped bool
|
stopped bool
|
||||||
|
|
||||||
|
// lastRefresh — время последнего успешного refresh/acquire лока.
|
||||||
|
// Нужен для сброса лидерства при длительном сбое refresh (split-brain).
|
||||||
|
lastRefresh time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// New — создаёт Elector. key — ключ лока, id — уникальный id инстанса.
|
// New — создаёт Elector. key — ключ лока, id — уникальный id инстанса.
|
||||||
func New(key, id string, ttl, interval time.Duration) *Elector {
|
func New(key, id string, ttl, interval time.Duration) *Elector {
|
||||||
return &Elector{
|
return &Elector{
|
||||||
key: key,
|
key: key,
|
||||||
id: id,
|
id: id,
|
||||||
ttl: ttl,
|
ttl: ttl,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
|
lastRefresh: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +113,15 @@ func (e *Elector) step() {
|
|||||||
ok, err := persistence.RefreshLock(e.key, e.id, e.ttl)
|
ok, err := persistence.RefreshLock(e.key, e.id, e.ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("leader: refresh: %v", err)
|
log.Errorf("leader: refresh: %v", err)
|
||||||
|
// Refresh не удаётся: если с последнего успеха прошло > TTL — лок
|
||||||
|
// гарантированно истёк, и другой инстанс мог стать лидером (split-brain).
|
||||||
|
// Сбрасываем лидерство (fail-safe), а не остаёмся «лидером» без лока.
|
||||||
|
if time.Since(e.lastRefreshAt()) > e.ttl {
|
||||||
|
e.setLeader(false)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
e.markRefreshed()
|
||||||
if !ok {
|
if !ok {
|
||||||
e.setLeader(false)
|
e.setLeader(false)
|
||||||
}
|
}
|
||||||
@@ -122,5 +134,18 @@ func (e *Elector) step() {
|
|||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
e.setLeader(true)
|
e.setLeader(true)
|
||||||
|
e.markRefreshed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Elector) lastRefreshAt() time.Time {
|
||||||
|
e.mu.RLock()
|
||||||
|
defer e.mu.RUnlock()
|
||||||
|
return e.lastRefresh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Elector) markRefreshed() {
|
||||||
|
e.mu.Lock()
|
||||||
|
e.lastRefresh = time.Now()
|
||||||
|
e.mu.Unlock()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user