Трапи: приймач каже про готовність тоді, коли справді готовий
Перший прогін CI завалив роботу agent на TestInformIsAcknowledged: «connection refused» від власного слухача. Локально той самий тест проходив — тобто плаваючий, а плаваючий тест отруює CI сильніше за відсутність CI: він привчає не дивитись на червоне. Причина — гонка в тесті. Run піднімається в горутині, а відправник шле одразу, не чекаючи. Між стартом горутини й зайняттям сокета є вікно, і датаграма, що в нього потрапила, отримує від ядра «port unreachable». На вільній машині вікно програє гонку майже завжди, під навантаженням раннера — виграє. Заодно знайшлось гірше, і вже не в тесті: рядок «приймач трапів слухає» друкувався ПЕРЕД tl.Listen(). Тобто журнал стверджував успіх до спроби, і навіть тоді, коли порт зайняти не вдалося, — а для 162 це саме той випадок, коли приймач, який мовчить, виглядає точнісінько як спокійна мережа. Тепер повідомлення йде після фактичного зайняття, і поруч з'явився Listening(): канал, який закривається тоді ж. Перевірено 20 прогонів поспіль — зелено. І окремо варте уваги: цю ваду не спіймала б жодна з наших перевірок, окрім справжнього CI під навантаженням. Він виправдав себе на першому ж запуску.
This commit is contained in:
parent
4b77c14168
commit
e98d51fe76
2 changed files with 48 additions and 1 deletions
|
|
@ -95,6 +95,10 @@ type Receiver struct {
|
|||
unknownSource atomic.Uint64
|
||||
|
||||
notify chan struct{}
|
||||
|
||||
// listening закривається, коли сокет зайнято. Окремо від notify:
|
||||
// той про події в черзі, цей — про готовність приймати їх узагалі.
|
||||
listening chan struct{}
|
||||
}
|
||||
|
||||
// New створює приймач. Порожня адреса означає DefaultAddr.
|
||||
|
|
@ -107,6 +111,8 @@ func New(addr string, log *slog.Logger) *Receiver {
|
|||
log: log,
|
||||
buckets: map[string]*bucket{},
|
||||
notify: make(chan struct{}, 1),
|
||||
|
||||
listening: make(chan struct{}),
|
||||
}
|
||||
r.perSource.Store(defaultPerSource)
|
||||
return r
|
||||
|
|
@ -203,7 +209,24 @@ func (r *Receiver) Run(ctx context.Context) error {
|
|||
tl.Close()
|
||||
}()
|
||||
|
||||
r.log.Info("приймач трапів слухає", "адреса", r.addr)
|
||||
// Про готовність повідомляємо ПІСЛЯ того, як сокет справді зайнято,
|
||||
// а не перед спробою. Раніше рядок «приймач трапів слухає» друкувався
|
||||
// до Listen — тобто журнал стверджував успіх навіть тоді, коли порт
|
||||
// зайняти не вдалося, і єдиний слід поразки губився серед рядків про
|
||||
// вдалий старт. Для 162 це особливо погано: приймач, який не слухає,
|
||||
// виглядає точнісінько як спокійна мережа.
|
||||
go func() {
|
||||
select {
|
||||
case <-tl.Listening():
|
||||
r.log.Info("приймач трапів слухає", "адреса", r.addr)
|
||||
close(r.listening)
|
||||
case <-ctx.Done():
|
||||
// Listen так і не піднявся — канал лишається відкритим, і
|
||||
// той, хто на нього чекає, побачить це як зупинку, а не як
|
||||
// готовність.
|
||||
}
|
||||
}()
|
||||
|
||||
if err := tl.Listen(r.addr); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
|
|
@ -213,6 +236,16 @@ func (r *Receiver) Run(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Listening закривається, коли сокет справді зайнято.
|
||||
//
|
||||
// Потрібне тим, хто шле одразу після старту: між викликом Run і
|
||||
// зайняттям порту є вікно, і датаграма, що потрапила в нього, не
|
||||
// губиться тихо — ядро відповідає «port unreachable». У тесті це
|
||||
// виглядало як плаваюча помилка «connection refused» раз на кілька
|
||||
// прогонів, тобто найгірший різновид: на вільній машині зелено, під
|
||||
// навантаженням CI червоно.
|
||||
func (r *Receiver) Listening() <-chan struct{} { return r.listening }
|
||||
|
||||
// Handle розбирає й кладе в чергу один трап.
|
||||
//
|
||||
// Окремо від Run, щоб приймач можна було перевірити без сокета.
|
||||
|
|
|
|||
|
|
@ -353,6 +353,20 @@ func TestInformIsAcknowledged(t *testing.T) {
|
|||
done := make(chan error, 1)
|
||||
go func() { done <- r.Run(ctx) }()
|
||||
|
||||
// Чекаємо, доки сокет справді зайнято.
|
||||
//
|
||||
// Без цього тест плаває: між стартом горутини й зайняттям порту є
|
||||
// вікно, і датаграма, що в нього потрапила, отримує від ядра
|
||||
// «port unreachable» — у вигляді «connection refused» на боці
|
||||
// відправника. На вільній машині вікно програє гонку майже завжди,
|
||||
// під навантаженням CI — виграє. Саме так це й знайшлось: тест був
|
||||
// зелений локально й червоний у першому ж прогоні на раннері.
|
||||
select {
|
||||
case <-r.Listening():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("приймач не зайняв порт за 5 с")
|
||||
}
|
||||
|
||||
sender := &gosnmp.GoSNMP{
|
||||
Target: "127.0.0.1",
|
||||
Port: uint16(portOf(t, addr)),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue