瀏覽代碼

refactor idle timeouts

tags/v0.10.0
Shivaram Lingamneni 6 年之前
父節點
當前提交
e540fde816
共有 5 個文件被更改,包括 189 次插入55 次删除
  1. 23
    52
      irc/client.go
  2. 1
    2
      irc/commands.go
  3. 12
    0
      irc/getters.go
  4. 153
    0
      irc/idletimer.go
  5. 0
    1
      irc/server.go

+ 23
- 52
irc/client.go 查看文件

@@ -25,17 +25,17 @@ import (
25 25
 )
26 26
 
27 27
 const (
28
-	// IdleTimeout is how long without traffic before a client's considered idle.
28
+	// RegisterTimeout is how long clients have to register before we disconnect them
29
+	RegisterTimeout = time.Minute
30
+	// IdleTimeout is how long without traffic before a registered client is considered idle.
29 31
 	IdleTimeout = time.Minute + time.Second*30
30
-	// QuitTimeout is how long without traffic (after they're considered idle) that clients are killed.
32
+	// QuitTimeout is how long without traffic before an idle client is disconnected
31 33
 	QuitTimeout = time.Minute
32 34
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
33 35
 	IdentTimeoutSeconds = 1.5
34 36
 )
35 37
 
36 38
 var (
37
-	// TimeoutStatedSeconds is how many seconds before clients are timed out (IdleTimeout plus QuitTimeout).
38
-	TimeoutStatedSeconds = strconv.Itoa(int((IdleTimeout + QuitTimeout).Seconds()))
39 39
 	// ErrNickAlreadySet is a weird error that's sent when the server's consistency has been compromised.
40 40
 	ErrNickAlreadySet = errors.New("Nickname is already set")
41 41
 )
@@ -58,7 +58,7 @@ type Client struct {
58 58
 	hasQuit            bool
59 59
 	hops               int
60 60
 	hostname           string
61
-	idleTimer          *time.Timer
61
+	idletimer          *IdleTimer
62 62
 	isDestroyed        bool
63 63
 	isQuitting         bool
64 64
 	nick               string
@@ -68,8 +68,6 @@ type Client struct {
68 68
 	operName           string
69 69
 	proxiedIP          string // actual remote IP if using the PROXY protocol
70 70
 	quitMessage        string
71
-	quitMessageSent    bool
72
-	quitTimer          *time.Timer
73 71
 	rawHostname        string
74 72
 	realname           string
75 73
 	registered         bool
@@ -79,7 +77,6 @@ type Client struct {
79 77
 	server             *Server
80 78
 	socket             *Socket
81 79
 	stateMutex         sync.RWMutex // generic protection for mutable state
82
-	timerMutex         sync.Mutex
83 80
 	username           string
84 81
 	vhost              string
85 82
 	whoisLine          string
@@ -140,7 +137,6 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
140 137
 			client.Notice("*** Could not find your username")
141 138
 		}
142 139
 	}
143
-	client.Touch()
144 140
 	go client.run()
145 141
 
146 142
 	return client
@@ -194,6 +190,9 @@ func (client *Client) run() {
194 190
 	var line string
195 191
 	var msg ircmsg.IrcMessage
196 192
 
193
+	client.idletimer = NewIdleTimer(client)
194
+	client.idletimer.Start()
195
+
197 196
 	// Set the hostname for this client
198 197
 	// (may be overridden by a later PROXY command from stunnel)
199 198
 	client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())
@@ -247,44 +246,15 @@ func (client *Client) Active() {
247 246
 }
248 247
 
249 248
 // Touch marks the client as alive (as it it has a connection to us and we
250
-// can receive messages from it), and resets when we'll send the client a
251
-// keepalive PING.
249
+// can receive messages from it).
252 250
 func (client *Client) Touch() {
253
-	client.timerMutex.Lock()
254
-	defer client.timerMutex.Unlock()
255
-
256
-	if client.quitTimer != nil {
257
-		client.quitTimer.Stop()
258
-	}
259
-
260
-	if client.idleTimer == nil {
261
-		client.idleTimer = time.AfterFunc(IdleTimeout, client.connectionIdle)
262
-	} else {
263
-		client.idleTimer.Reset(IdleTimeout)
264
-	}
251
+	client.idletimer.Touch()
265 252
 }
266 253
 
267
-// connectionIdle is run when the client has not sent us any data for a while,
268
-// sends the client a PING and starts the quit timeout.
269
-func (client *Client) connectionIdle() {
270
-	client.timerMutex.Lock()
271
-	defer client.timerMutex.Unlock()
272
-
254
+// Ping sends the client a PING message.
255
+func (client *Client) Ping() {
273 256
 	client.Send(nil, "", "PING", client.nick)
274 257
 
275
-	if client.quitTimer == nil {
276
-		client.quitTimer = time.AfterFunc(QuitTimeout, client.connectionTimeout)
277
-	} else {
278
-		client.quitTimer.Reset(QuitTimeout)
279
-	}
280
-}
281
-
282
-// connectionTimeout runs after connectionIdle has been run, if we do not receive a
283
-// ping or any other activity back from the client. When this happens we assume the
284
-// connection has died and remove the client from the network.
285
-func (client *Client) connectionTimeout() {
286
-	client.Quit(fmt.Sprintf("Ping timeout: %s seconds", TimeoutStatedSeconds))
287
-	client.isQuitting = true
288 258
 }
289 259
 
290 260
 //
@@ -293,12 +263,16 @@ func (client *Client) connectionTimeout() {
293 263
 
294 264
 // Register sets the client details as appropriate when entering the network.
295 265
 func (client *Client) Register() {
296
-	if client.registered {
266
+	client.stateMutex.Lock()
267
+	alreadyRegistered := client.registered
268
+	client.registered = true
269
+	client.stateMutex.Unlock()
270
+
271
+	if alreadyRegistered {
297 272
 		return
298 273
 	}
299
-	client.registered = true
300
-	client.Touch()
301 274
 
275
+	client.Touch()
302 276
 	client.updateNickMask("")
303 277
 	client.server.monitorManager.AlertAbout(client, true)
304 278
 }
@@ -504,9 +478,9 @@ func (client *Client) RplISupport() {
504 478
 // Quit sends the given quit message to the client (but does not destroy them).
505 479
 func (client *Client) Quit(message string) {
506 480
 	client.stateMutex.Lock()
507
-	alreadyQuit := client.quitMessageSent
481
+	alreadyQuit := client.isQuitting
508 482
 	if !alreadyQuit {
509
-		client.quitMessageSent = true
483
+		client.isQuitting = true
510 484
 		client.quitMessage = message
511 485
 	}
512 486
 	client.stateMutex.Unlock()
@@ -567,11 +541,8 @@ func (client *Client) destroy() {
567 541
 	client.server.clients.Remove(client)
568 542
 
569 543
 	// clean up self
570
-	if client.idleTimer != nil {
571
-		client.idleTimer.Stop()
572
-	}
573
-	if client.quitTimer != nil {
574
-		client.quitTimer.Stop()
544
+	if client.idletimer != nil {
545
+		client.idletimer.Stop()
575 546
 	}
576 547
 
577 548
 	client.socket.Close()

+ 1
- 2
irc/commands.go 查看文件

@@ -39,8 +39,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
39 39
 	if !cmd.leaveClientActive {
40 40
 		client.Active()
41 41
 	}
42
-	// only touch client if they're registered so that unregistered clients timeout appropriately
43
-	if client.registered && !cmd.leaveClientIdle {
42
+	if !cmd.leaveClientIdle {
44 43
 		client.Touch()
45 44
 	}
46 45
 	exiting := cmd.handler(server, client, msg)

+ 12
- 0
irc/getters.go 查看文件

@@ -40,3 +40,15 @@ func (client *Client) getNickCasefolded() string {
40 40
 	defer client.stateMutex.RUnlock()
41 41
 	return client.nickCasefolded
42 42
 }
43
+
44
+func (client *Client) Registered() bool {
45
+	client.stateMutex.RLock()
46
+	defer client.stateMutex.RUnlock()
47
+	return client.registered
48
+}
49
+
50
+func (client *Client) Destroyed() bool {
51
+	client.stateMutex.RLock()
52
+	defer client.stateMutex.RUnlock()
53
+	return client.isDestroyed
54
+}

+ 153
- 0
irc/idletimer.go 查看文件

@@ -0,0 +1,153 @@
1
+// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"fmt"
8
+	"sync"
9
+	"time"
10
+)
11
+
12
+// client idleness state machine
13
+
14
+type TimerState uint
15
+
16
+const (
17
+	TimerUnregistered TimerState = iota // client is unregistered
18
+	TimerActive                         // client is actively sending commands
19
+	TimerIdle                           // client is idle, we sent PING and are waiting for PONG
20
+)
21
+
22
+type IdleTimer struct {
23
+	sync.Mutex
24
+
25
+	// immutable after construction
26
+	registerTimeout time.Duration
27
+	idleTimeout     time.Duration
28
+	quitTimeout     time.Duration
29
+
30
+	// mutable
31
+	client   *Client
32
+	state    TimerState
33
+	lastSeen time.Time
34
+}
35
+
36
+// NewIdleTimer sets up a new IdleTimer using constant timeouts.
37
+func NewIdleTimer(client *Client) *IdleTimer {
38
+	it := IdleTimer{
39
+		registerTimeout: RegisterTimeout,
40
+		idleTimeout:     IdleTimeout,
41
+		quitTimeout:     QuitTimeout,
42
+		client:          client,
43
+		state:           TimerUnregistered,
44
+	}
45
+	return &it
46
+}
47
+
48
+// Start starts counting idle time; if there is no activity from the client,
49
+// it will eventually be stopped.
50
+func (it *IdleTimer) Start() {
51
+	it.Lock()
52
+	it.lastSeen = time.Now()
53
+	it.Unlock()
54
+	go it.mainLoop()
55
+}
56
+
57
+func (it *IdleTimer) mainLoop() {
58
+	for {
59
+		it.Lock()
60
+		client := it.client
61
+		state := it.state
62
+		lastSeen := it.lastSeen
63
+		it.Unlock()
64
+
65
+		if client == nil {
66
+			return
67
+		}
68
+
69
+		registered := client.Registered()
70
+		now := time.Now()
71
+		idleTime := now.Sub(lastSeen)
72
+		newState := state
73
+
74
+		switch state {
75
+		case TimerUnregistered:
76
+			if registered {
77
+				// transition to TimerActive state
78
+				newState = TimerActive
79
+			}
80
+		case TimerActive:
81
+			if idleTime >= IdleTimeout {
82
+				newState = TimerIdle
83
+				client.Ping()
84
+			}
85
+		case TimerIdle:
86
+			if idleTime < IdleTimeout {
87
+				// new ping came in after we transitioned to TimerIdle
88
+				newState = TimerActive
89
+			}
90
+		}
91
+
92
+		it.Lock()
93
+		it.state = newState
94
+		it.Unlock()
95
+
96
+		var nextSleep time.Duration
97
+		switch newState {
98
+		case TimerUnregistered:
99
+			nextSleep = it.registerTimeout - idleTime
100
+		case TimerActive:
101
+			nextSleep = it.idleTimeout - idleTime
102
+		case TimerIdle:
103
+			nextSleep = (it.idleTimeout + it.quitTimeout) - idleTime
104
+		}
105
+
106
+		if nextSleep <= 0 {
107
+			// ran out of time, hang them up
108
+			client.Quit(it.quitMessage(newState))
109
+			client.destroy()
110
+			return
111
+		}
112
+
113
+		time.Sleep(nextSleep)
114
+	}
115
+}
116
+
117
+// Touch registers activity (e.g., sending a command) from an client.
118
+func (it *IdleTimer) Touch() {
119
+	it.Lock()
120
+	client := it.client
121
+	it.Unlock()
122
+
123
+	// ignore touches for unregistered clients
124
+	if client != nil && !client.Registered() {
125
+		return
126
+	}
127
+
128
+	it.Lock()
129
+	it.lastSeen = time.Now()
130
+	it.Unlock()
131
+}
132
+
133
+// Stop stops counting idle time.
134
+func (it *IdleTimer) Stop() {
135
+	it.Lock()
136
+	defer it.Unlock()
137
+	// no need to stop the goroutine, it'll clean itself up in a few minutes;
138
+	// just ensure the Client object is collectable
139
+	it.client = nil
140
+}
141
+
142
+func (it *IdleTimer) quitMessage(state TimerState) string {
143
+	switch state {
144
+	case TimerUnregistered:
145
+		return fmt.Sprintf("Registration timeout: %v", it.registerTimeout)
146
+	case TimerIdle:
147
+		// how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout).
148
+		return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
149
+	default:
150
+		// shouldn't happen
151
+		return ""
152
+	}
153
+}

+ 0
- 1
irc/server.go 查看文件

@@ -416,7 +416,6 @@ func (server *Server) tryRegister(c *Client) {
416 416
 			reason += fmt.Sprintf(" [%s]", info.Time.Duration.String())
417 417
 		}
418 418
 		c.Send(nil, "", "ERROR", fmt.Sprintf("You are banned from this server (%s)", reason))
419
-		c.quitMessageSent = true
420 419
 		c.destroy()
421 420
 		return
422 421
 	}

Loading…
取消
儲存