Add tests for channel join retry logic

Also adopt interface for querying time information, so it can be faked
properly during at test time

Signed-off-by: Luca Bigliardi <shammash@google.com>
This commit is contained in:
Luca Bigliardi 2021-03-29 16:06:36 +02:00
parent e7d5eefc49
commit 559b817262
10 changed files with 202 additions and 60 deletions

View File

@ -50,22 +50,6 @@ func jitterFunc(input int) int {
return rand.Intn(input) return rand.Intn(input)
} }
// TimeTeller interface allows injection of fake time during testing
type TimeTeller interface {
Now() time.Time
After(time.Duration) <-chan time.Time
}
type RealTime struct{}
func (r *RealTime) Now() time.Time {
return time.Now()
}
func (r *RealTime) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
type BackoffMaker struct{} type BackoffMaker struct{}
func (bm *BackoffMaker) NewDelayer(maxBackoff float64, resetDelta float64, durationUnit time.Duration) Delayer { func (bm *BackoffMaker) NewDelayer(maxBackoff float64, resetDelta float64, durationUnit time.Duration) Delayer {

View File

@ -20,24 +20,6 @@ import (
"time" "time"
) )
type FakeTime struct {
timeseries []int
lastIndex int
durationUnit time.Duration
afterChan chan time.Time
}
func (f *FakeTime) Now() time.Time {
timeDelta := time.Duration(f.timeseries[f.lastIndex]) * f.durationUnit
fakeTime := time.Unix(0, 0).Add(timeDelta)
f.lastIndex++
return fakeTime
}
func (f *FakeTime) After(d time.Duration) <-chan time.Time {
return f.afterChan
}
func FakeJitter(input int) int { func FakeJitter(input int) int {
return input return input
} }

37
fake_timeteller.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"time"
)
type FakeTime struct {
timeseries []int
lastIndex int
durationUnit time.Duration
afterChan chan time.Time
}
func (f *FakeTime) Now() time.Time {
timeDelta := time.Duration(f.timeseries[f.lastIndex]) * f.durationUnit
fakeTime := time.Unix(0, 0).Add(timeDelta)
f.lastIndex++
return fakeTime
}
func (f *FakeTime) After(d time.Duration) <-chan time.Time {
return f.afterChan
}

10
irc.go
View File

@ -100,9 +100,10 @@ type IRCNotifier struct {
NickservDelayWait time.Duration NickservDelayWait time.Duration
BackoffCounter Delayer BackoffCounter Delayer
timeTeller TimeTeller
} }
func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker DelayerMaker) (*IRCNotifier, error) { func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker DelayerMaker, timeTeller TimeTeller) (*IRCNotifier, error) {
ircConfig := makeGOIRCConfig(config) ircConfig := makeGOIRCConfig(config)
@ -112,7 +113,7 @@ func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker Delaye
ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs, ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs,
time.Second) time.Second)
channelReconciler := NewChannelReconciler(config, client, delayerMaker) channelReconciler := NewChannelReconciler(config, client, delayerMaker, timeTeller)
notifier := &IRCNotifier{ notifier := &IRCNotifier{
Nick: config.IRCNick, Nick: config.IRCNick,
@ -125,6 +126,7 @@ func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker Delaye
UsePrivmsg: config.UsePrivmsg, UsePrivmsg: config.UsePrivmsg,
NickservDelayWait: nickservWaitSecs * time.Second, NickservDelayWait: nickservWaitSecs * time.Second,
BackoffCounter: backoffCounter, BackoffCounter: backoffCounter,
timeTeller: timeTeller,
} }
notifier.registerHandlers() notifier.registerHandlers()
@ -183,7 +185,7 @@ func (n *IRCNotifier) ChannelJoined(ctx context.Context, channel string) bool {
select { select {
case <-waitJoined: case <-waitJoined:
return true return true
case <-time.After(ircJoinWaitSecs * time.Second): case <-n.timeTeller.After(ircJoinWaitSecs * time.Second):
log.Printf("Channel %s not joined after %d seconds, giving bad news to caller", channel, ircJoinWaitSecs) log.Printf("Channel %s not joined after %d seconds, giving bad news to caller", channel, ircJoinWaitSecs)
return false return false
case <-ctx.Done(): case <-ctx.Done():
@ -220,7 +222,7 @@ func (n *IRCNotifier) ShutdownPhase() {
log.Printf("Wait for IRC disconnect to complete") log.Printf("Wait for IRC disconnect to complete")
select { select {
case <-n.sessionDownSignal: case <-n.sessionDownSignal:
case <-time.After(n.Client.Config().Timeout): case <-n.timeTeller.After(n.Client.Config().Timeout):
log.Printf("Timeout while waiting for IRC disconnect to complete, stopping anyway") log.Printf("Timeout while waiting for IRC disconnect to complete, stopping anyway")
} }
n.sessionWg.Done() n.sessionWg.Done()

View File

@ -16,6 +16,7 @@ package main
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -130,6 +131,17 @@ func (s *testServer) handleConnection(conn net.Conn) {
} }
} }
func (s *testServer) SendMsg(msg string) error {
if s.Client == nil {
return errors.New("Cannot write without client connected")
}
bufConn := bufio.NewWriter(s.Client)
log.Printf("=Server= sending to client: %s", msg)
_, err := bufConn.WriteString(msg)
bufConn.Flush()
return err
}
func (s *testServer) SetCloseEarly(h closeEarlyHandler) { func (s *testServer) SetCloseEarly(h closeEarlyHandler) {
s.closeEarlyMu.Lock() s.closeEarlyMu.Lock()
defer s.closeEarlyMu.Unlock() defer s.closeEarlyMu.Unlock()

View File

@ -44,11 +44,14 @@ func makeTestIRCConfig(IRCPort int) *Config {
func makeTestNotifier(t *testing.T, config *Config) (*IRCNotifier, chan AlertMsg, context.Context, context.CancelFunc, *sync.WaitGroup) { func makeTestNotifier(t *testing.T, config *Config) (*IRCNotifier, chan AlertMsg, context.Context, context.CancelFunc, *sync.WaitGroup) {
fakeDelayerMaker := &FakeDelayerMaker{} fakeDelayerMaker := &FakeDelayerMaker{}
fakeTime := &FakeTime{
afterChan: make(chan time.Time, 1),
}
alertMsgs := make(chan AlertMsg) alertMsgs := make(chan AlertMsg)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
stopWg := sync.WaitGroup{} stopWg := sync.WaitGroup{}
stopWg.Add(1) stopWg.Add(1)
notifier, err := NewIRCNotifier(config, alertMsgs, fakeDelayerMaker) notifier, err := NewIRCNotifier(config, alertMsgs, fakeDelayerMaker, fakeTime)
if err != nil { if err != nil {
t.Fatal(fmt.Sprintf("Could not create IRC notifier: %s", err)) t.Fatal(fmt.Sprintf("Could not create IRC notifier: %s", err))
} }

View File

@ -40,7 +40,7 @@ func main() {
alertMsgs := make(chan AlertMsg, config.AlertBufferSize) alertMsgs := make(chan AlertMsg, config.AlertBufferSize)
stopWg.Add(1) stopWg.Add(1)
ircNotifier, err := NewIRCNotifier(config, alertMsgs, &BackoffMaker{}) ircNotifier, err := NewIRCNotifier(config, alertMsgs, &BackoffMaker{}, &RealTime{})
if err != nil { if err != nil {
log.Printf("Could not create IRC notifier: %s", err) log.Printf("Could not create IRC notifier: %s", err)
return return

View File

@ -32,7 +32,9 @@ const (
type channelState struct { type channelState struct {
channel IRCChannel channel IRCChannel
client *irc.Conn client *irc.Conn
delayer Delayer
delayer Delayer
timeTeller TimeTeller
joinDone chan struct{} // joined when channel is closed joinDone chan struct{} // joined when channel is closed
joined bool joined bool
@ -42,13 +44,14 @@ type channelState struct {
mu sync.Mutex mu sync.Mutex
} }
func newChannelState(channel *IRCChannel, client *irc.Conn, delayerMaker DelayerMaker) *channelState { func newChannelState(channel *IRCChannel, client *irc.Conn, delayerMaker DelayerMaker, timeTeller TimeTeller) *channelState {
delayer := delayerMaker.NewDelayer(ircJoinMaxBackoffSecs, ircJoinBackoffResetSecs, time.Second) delayer := delayerMaker.NewDelayer(ircJoinMaxBackoffSecs, ircJoinBackoffResetSecs, time.Second)
return &channelState{ return &channelState{
channel: *channel, channel: *channel,
client: client, client: client,
delayer: delayer, delayer: delayer,
timeTeller: timeTeller,
joinDone: make(chan struct{}), joinDone: make(chan struct{}),
joined: false, joined: false,
joinUnsetSignal: make(chan bool), joinUnsetSignal: make(chan bool),
@ -108,7 +111,7 @@ func (c *channelState) join(ctx context.Context) {
select { select {
case <-c.JoinDone(): case <-c.JoinDone():
log.Printf("Channel %s monitor: join succeeded", c.channel.Name) log.Printf("Channel %s monitor: join succeeded", c.channel.Name)
case <-time.After(ircJoinWaitSecs * time.Second): case <-c.timeTeller.After(ircJoinWaitSecs * time.Second):
log.Printf("Channel %s monitor: could not join after %d seconds, will retry", c.channel.Name, ircJoinWaitSecs) log.Printf("Channel %s monitor: could not join after %d seconds, will retry", c.channel.Name, ircJoinWaitSecs)
case <-ctx.Done(): case <-ctx.Done():
log.Printf("Channel %s monitor: context canceled while waiting for join", c.channel.Name) log.Printf("Channel %s monitor: context canceled while waiting for join", c.channel.Name)
@ -147,6 +150,7 @@ type ChannelReconciler struct {
client *irc.Conn client *irc.Conn
delayerMaker DelayerMaker delayerMaker DelayerMaker
timeTeller TimeTeller
channels map[string]*channelState channels map[string]*channelState
@ -157,11 +161,12 @@ type ChannelReconciler struct {
mu sync.Mutex mu sync.Mutex
} }
func NewChannelReconciler(config *Config, client *irc.Conn, delayerMaker DelayerMaker) *ChannelReconciler { func NewChannelReconciler(config *Config, client *irc.Conn, delayerMaker DelayerMaker, timeTeller TimeTeller) *ChannelReconciler {
reconciler := &ChannelReconciler{ reconciler := &ChannelReconciler{
preJoinChannels: config.IRCChannels, preJoinChannels: config.IRCChannels,
client: client, client: client,
delayerMaker: delayerMaker, delayerMaker: delayerMaker,
timeTeller: timeTeller,
channels: make(map[string]*channelState), channels: make(map[string]*channelState),
} }
@ -219,7 +224,7 @@ func (r *ChannelReconciler) HandleKick(nick string, channel string) {
} }
func (r *ChannelReconciler) unsafeAddChannel(channel *IRCChannel) *channelState { func (r *ChannelReconciler) unsafeAddChannel(channel *IRCChannel) *channelState {
c := newChannelState(channel, r.client, r.delayerMaker) c := newChannelState(channel, r.client, r.delayerMaker, r.timeTeller)
r.stopWg.Add(1) r.stopWg.Add(1)
go c.Monitor(r.stopCtx, &r.stopWg) go c.Monitor(r.stopCtx, &r.stopWg)

View File

@ -21,21 +21,12 @@ import (
"sort" "sort"
"sync" "sync"
"testing" "testing"
"time"
irc "github.com/fluffle/goirc/client" irc "github.com/fluffle/goirc/client"
) )
func makeReconcilerTestIRCConfig(IRCPort int) *Config { func makeTestReconciler(config *Config) (*ChannelReconciler, chan bool, chan bool, *FakeTime) {
config := makeTestIRCConfig(IRCPort)
config.IRCChannels = []IRCChannel{
IRCChannel{Name: "#foo"},
IRCChannel{Name: "#bar"},
IRCChannel{Name: "#baz"},
}
return config
}
func makeTestReconciler(config *Config) (*ChannelReconciler, chan bool, chan bool) {
sessionUp := make(chan bool) sessionUp := make(chan bool)
sessionDown := make(chan bool) sessionDown := make(chan bool)
@ -53,15 +44,23 @@ func makeTestReconciler(config *Config) (*ChannelReconciler, chan bool, chan boo
}) })
fakeDelayerMaker := &FakeDelayerMaker{} fakeDelayerMaker := &FakeDelayerMaker{}
reconciler := NewChannelReconciler(config, client, fakeDelayerMaker) fakeTime := &FakeTime{
afterChan: make(chan time.Time, 1),
}
reconciler := NewChannelReconciler(config, client, fakeDelayerMaker, fakeTime)
return reconciler, sessionUp, sessionDown return reconciler, sessionUp, sessionDown, fakeTime
} }
func TestPreJoinChannels(t *testing.T) { func TestPreJoinChannels(t *testing.T) {
server, port := makeTestServer(t) server, port := makeTestServer(t)
config := makeReconcilerTestIRCConfig(port) config := makeTestIRCConfig(port)
reconciler, sessionUp, sessionDown := makeTestReconciler(config) config.IRCChannels = []IRCChannel{
IRCChannel{Name: "#foo"},
IRCChannel{Name: "#bar"},
IRCChannel{Name: "#baz"},
}
reconciler, sessionUp, sessionDown, _ := makeTestReconciler(config)
var testStep sync.WaitGroup var testStep sync.WaitGroup
@ -98,3 +97,86 @@ func TestPreJoinChannels(t *testing.T) {
t.Error("Did not pre-join channels") t.Error("Did not pre-join channels")
} }
} }
func TestKeepJoining(t *testing.T) {
server, port := makeTestServer(t)
config := makeTestIRCConfig(port)
reconciler, sessionUp, sessionDown, fakeTime := makeTestReconciler(config)
var testStep sync.WaitGroup
var joinedCounter int
// Confirm join only after a few attempts
joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error {
joinedCounter++
if joinedCounter == 3 {
testStep.Done()
return hJOIN(conn, line)
} else {
fakeTime.afterChan <- time.Now()
}
return nil
}
server.SetHandler("JOIN", joinHandler)
testStep.Add(1)
reconciler.client.Connect()
<-sessionUp
reconciler.Start(context.Background())
testStep.Wait()
reconciler.client.Quit("see ya")
<-sessionDown
reconciler.Stop()
server.Stop()
expectedJoinedCounter := 3
if !reflect.DeepEqual(expectedJoinedCounter, joinedCounter) {
t.Error("Did not keep joining")
}
}
func TestKickRejoin(t *testing.T) {
server, port := makeTestServer(t)
config := makeTestIRCConfig(port)
reconciler, sessionUp, sessionDown, _ := makeTestReconciler(config)
var testStep sync.WaitGroup
// Wait for channel to be joined
joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error {
hJOIN(conn, line)
testStep.Done()
return nil
}
server.SetHandler("JOIN", joinHandler)
testStep.Add(1)
reconciler.client.Connect()
<-sessionUp
reconciler.Start(context.Background())
testStep.Wait()
// Kick and wait for channel to be joined again
testStep.Add(1)
server.SendMsg(":test!~test@example.com KICK #foo foo :Bye!\n")
testStep.Wait()
reconciler.client.Quit("see ya")
<-sessionDown
reconciler.Stop()
server.Stop()
}

35
time.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"time"
)
// TimeTeller interface allows injection of fake time during testing
type TimeTeller interface {
Now() time.Time
After(time.Duration) <-chan time.Time
}
type RealTime struct{}
func (r *RealTime) Now() time.Time {
return time.Now()
}
func (r *RealTime) After(d time.Duration) <-chan time.Time {
return time.After(d)
}