Browse Source

Merge pull request #1232 from slingamn/timeout_refactor.2

fix #1229
tags/v2.3.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
5e11f3b346
No account linked to committer's email address
3 changed files with 111 additions and 182 deletions
  1. 111
    5
      irc/client.go
  2. 0
    7
      irc/commands.go
  3. 0
    170
      irc/idletimer.go

+ 111
- 5
irc/client.go View File

@@ -40,6 +40,25 @@ const (
40 40
 	lastSeenWriteInterval = time.Hour
41 41
 )
42 42
 
43
+const (
44
+	// RegisterTimeout is how long clients have to register before we disconnect them
45
+	RegisterTimeout = time.Minute
46
+	// DefaultIdleTimeout is how long without traffic before we send the client a PING
47
+	DefaultIdleTimeout = time.Minute + 30*time.Second
48
+	// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
49
+	// (single-onion circuits will close unless the client sends data once every 60 seconds):
50
+	// https://bugs.torproject.org/29665
51
+	TorIdleTimeout = time.Second * 30
52
+	// This is how long a client gets without sending any message, including the PONG to our
53
+	// PING, before we disconnect them:
54
+	DefaultTotalTimeout = 2*time.Minute + 30*time.Second
55
+	// Resumeable clients (clients who have negotiated caps.Resume) get longer:
56
+	ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
57
+
58
+	// round off the ping interval by this much, see below:
59
+	PingCoalesceThreshold = time.Second
60
+)
61
+
43 62
 // ResumeDetails is a place to stash data at various stages of
44 63
 // the resume process: when handling the RESUME command itself,
45 64
 // when completing the registration, and when rejoining channels.
@@ -83,6 +102,7 @@ type Client struct {
83 102
 	realname           string
84 103
 	realIP             net.IP
85 104
 	registered         bool
105
+	registrationTimer  *time.Timer
86 106
 	resumeID           string
87 107
 	server             *Server
88 108
 	skeleton           string
@@ -123,7 +143,10 @@ type Session struct {
123 143
 	deviceID string
124 144
 
125 145
 	ctime      time.Time
126
-	lastActive time.Time
146
+	lastActive time.Time // last non-CTCP PRIVMSG sent; updates publicly visible idle time
147
+	lastTouch  time.Time // last line sent; updates timer for idle timeouts
148
+	idleTimer  *time.Timer
149
+	pingSent   bool // we sent PING to a putatively idle connection and we're waiting for PONG
127 150
 
128 151
 	socket      *Socket
129 152
 	realIP      net.IP
@@ -131,7 +154,6 @@ type Session struct {
131 154
 	rawHostname string
132 155
 	isTor       bool
133 156
 
134
-	idletimer            IdleTimer
135 157
 	fakelag              Fakelag
136 158
 	deferredFakelagCount int
137 159
 	destroyed            uint32
@@ -342,7 +364,6 @@ func (server *Server) RunClient(conn IRCConn) {
342 364
 	}
343 365
 	client.sessions = []*Session{session}
344 366
 
345
-	session.idletimer.Initialize(session)
346 367
 	session.resetFakelag()
347 368
 
348 369
 	if wConn.Secure {
@@ -363,6 +384,7 @@ func (server *Server) RunClient(conn IRCConn) {
363 384
 		}
364 385
 	}
365 386
 
387
+	client.registrationTimer = time.AfterFunc(RegisterTimeout, client.handleRegisterTimeout)
366 388
 	server.stats.Add()
367 389
 	client.run(session)
368 390
 }
@@ -605,7 +627,7 @@ func (client *Client) run(session *Session) {
605 627
 
606 628
 	isReattach := client.Registered()
607 629
 	if isReattach {
608
-		session.idletimer.Touch()
630
+		client.Touch(session)
609 631
 		if session.resumeDetails != nil {
610 632
 			session.playResume()
611 633
 			session.resumeDetails = nil
@@ -739,6 +761,7 @@ func (client *Client) Touch(session *Session) {
739 761
 			client.lastSeenLastWrite = now
740 762
 		}
741 763
 	}
764
+	client.updateIdleTimer(session, now)
742 765
 	client.stateMutex.Unlock()
743 766
 	if markDirty {
744 767
 		client.markDirty(IncludeLastSeen)
@@ -763,6 +786,75 @@ func (client *Client) setLastSeen(now time.Time, deviceID string) {
763 786
 	}
764 787
 }
765 788
 
789
+func (client *Client) updateIdleTimer(session *Session, now time.Time) {
790
+	session.lastTouch = now
791
+	session.pingSent = false
792
+
793
+	if session.idleTimer == nil {
794
+		pingTimeout := DefaultIdleTimeout
795
+		if session.isTor {
796
+			pingTimeout = TorIdleTimeout
797
+		}
798
+		session.idleTimer = time.AfterFunc(pingTimeout, session.handleIdleTimeout)
799
+	}
800
+}
801
+
802
+func (session *Session) handleIdleTimeout() {
803
+	totalTimeout := DefaultTotalTimeout
804
+	if session.capabilities.Has(caps.Resume) {
805
+		totalTimeout = ResumeableTotalTimeout
806
+	}
807
+	pingTimeout := DefaultIdleTimeout
808
+	if session.isTor {
809
+		pingTimeout = TorIdleTimeout
810
+	}
811
+
812
+	session.client.stateMutex.Lock()
813
+	now := time.Now()
814
+	timeUntilDestroy := session.lastTouch.Add(totalTimeout).Sub(now)
815
+	timeUntilPing := session.lastTouch.Add(pingTimeout).Sub(now)
816
+	shouldDestroy := session.pingSent && timeUntilDestroy <= 0
817
+	// XXX this should really be time <= 0, but let's do some hacky timer coalescing:
818
+	// a typical idling client will do nothing other than respond immediately to our pings,
819
+	// so we'll PING at t=0, they'll respond at t=0.05, then we'll wake up at t=90 and find
820
+	// that we need to PING again at t=90.05. Rather than wake up again, just send it now:
821
+	shouldSendPing := !session.pingSent && timeUntilPing <= PingCoalesceThreshold
822
+	if !shouldDestroy {
823
+		if shouldSendPing {
824
+			session.pingSent = true
825
+		}
826
+		// check in again at the minimum of these 3 possible intervals:
827
+		// 1. the ping timeout (assuming we PING and they reply immediately with PONG)
828
+		// 2. the next time we would send PING (if they don't send any more lines)
829
+		// 3. the next time we would destroy (if they don't send any more lines)
830
+		nextTimeout := pingTimeout
831
+		if PingCoalesceThreshold < timeUntilPing && timeUntilPing < nextTimeout {
832
+			nextTimeout = timeUntilPing
833
+		}
834
+		if 0 < timeUntilDestroy && timeUntilDestroy < nextTimeout {
835
+			nextTimeout = timeUntilDestroy
836
+		}
837
+		session.idleTimer.Stop()
838
+		session.idleTimer.Reset(nextTimeout)
839
+	}
840
+	session.client.stateMutex.Unlock()
841
+
842
+	if shouldDestroy {
843
+		session.client.Quit(fmt.Sprintf("Ping timeout: %v", totalTimeout), session)
844
+		session.client.destroy(session)
845
+	} else if shouldSendPing {
846
+		session.Ping()
847
+	}
848
+}
849
+
850
+func (session *Session) stopIdleTimer() {
851
+	session.client.stateMutex.Lock()
852
+	defer session.client.stateMutex.Unlock()
853
+	if session.idleTimer != nil {
854
+		session.idleTimer.Stop()
855
+	}
856
+}
857
+
766 858
 // Ping sends the client a PING message.
767 859
 func (session *Session) Ping() {
768 860
 	session.Send(nil, "", "PING", session.client.Nick())
@@ -1119,6 +1211,10 @@ func (client *Client) SetNick(nick, nickCasefolded, skeleton string) (success bo
1119 1211
 	} else if !client.registered {
1120 1212
 		// XXX test this before setting it to avoid annoying the race detector
1121 1213
 		client.registered = true
1214
+		if client.registrationTimer != nil {
1215
+			client.registrationTimer.Stop()
1216
+			client.registrationTimer = nil
1217
+		}
1122 1218
 	}
1123 1219
 	client.nick = nick
1124 1220
 	client.nickCasefolded = nickCasefolded
@@ -1294,6 +1390,11 @@ func (client *Client) destroy(session *Session) {
1294 1390
 		client.awayMessage = awayMessage
1295 1391
 	}
1296 1392
 
1393
+	if client.registrationTimer != nil {
1394
+		// unconditionally stop; if the client is still unregistered it must be destroyed
1395
+		client.registrationTimer.Stop()
1396
+	}
1397
+
1297 1398
 	client.stateMutex.Unlock()
1298 1399
 
1299 1400
 	// XXX there is no particular reason to persist this state here rather than
@@ -1311,7 +1412,7 @@ func (client *Client) destroy(session *Session) {
1311 1412
 			// session has been attached to a new client; do not destroy it
1312 1413
 			continue
1313 1414
 		}
1314
-		session.idletimer.Stop()
1415
+		session.stopIdleTimer()
1315 1416
 		// send quit/error message to client if they haven't been sent already
1316 1417
 		client.Quit("", session)
1317 1418
 		quitMessage = session.quitMessage
@@ -1693,6 +1794,11 @@ func (client *Client) historyStatus(config *Config) (status HistoryStatus, targe
1693 1794
 	return
1694 1795
 }
1695 1796
 
1797
+func (client *Client) handleRegisterTimeout() {
1798
+	client.Quit(fmt.Sprintf("Registration timeout: %v", RegisterTimeout), nil)
1799
+	client.destroy(nil)
1800
+}
1801
+
1696 1802
 func (client *Client) copyLastSeen() (result map[string]time.Time) {
1697 1803
 	client.stateMutex.RLock()
1698 1804
 	defer client.stateMutex.RUnlock()

+ 0
- 7
irc/commands.go View File

@@ -58,13 +58,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
58 58
 		exiting = server.tryRegister(client, session)
59 59
 	}
60 60
 
61
-	// most servers do this only for PING/PONG, but we'll do it for any command:
62
-	if client.registered {
63
-		// touch even if `exiting`, so we record the time of a QUIT accurately
64
-		session.idletimer.Touch()
65
-	}
66
-
67
-	// TODO: eliminate idletimer entirely in favor of this measurement
68 61
 	if client.registered {
69 62
 		client.Touch(session)
70 63
 	}

+ 0
- 170
irc/idletimer.go View File

@@ -4,179 +4,9 @@
4 4
 package irc
5 5
 
6 6
 import (
7
-	"fmt"
8
-	"sync"
9 7
 	"time"
10
-
11
-	"github.com/oragono/oragono/irc/caps"
12
-)
13
-
14
-const (
15
-	// RegisterTimeout is how long clients have to register before we disconnect them
16
-	RegisterTimeout = time.Minute
17
-	// DefaultIdleTimeout is how long without traffic before we send the client a PING
18
-	DefaultIdleTimeout = time.Minute + 30*time.Second
19
-	// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
20
-	// (single-onion circuits will close unless the client sends data once every 60 seconds):
21
-	// https://bugs.torproject.org/29665
22
-	TorIdleTimeout = time.Second * 30
23
-	// This is how long a client gets without sending any message, including the PONG to our
24
-	// PING, before we disconnect them:
25
-	DefaultTotalTimeout = 2*time.Minute + 30*time.Second
26
-	// Resumeable clients (clients who have negotiated caps.Resume) get longer:
27
-	ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
28
-)
29
-
30
-// client idleness state machine
31
-
32
-type TimerState uint
33
-
34
-const (
35
-	TimerUnregistered TimerState = iota // client is unregistered
36
-	TimerActive                         // client is actively sending commands
37
-	TimerIdle                           // client is idle, we sent PING and are waiting for PONG
38
-	TimerDead                           // client was terminated
39 8
 )
40 9
 
41
-type IdleTimer struct {
42
-	sync.Mutex // tier 1
43
-
44
-	// immutable after construction
45
-	registerTimeout time.Duration
46
-	session         *Session
47
-
48
-	// mutable
49
-	idleTimeout time.Duration
50
-	quitTimeout time.Duration
51
-	state       TimerState
52
-	timer       *time.Timer
53
-}
54
-
55
-// Initialize sets up an IdleTimer and starts counting idle time;
56
-// if there is no activity from the client, it will eventually be stopped.
57
-func (it *IdleTimer) Initialize(session *Session) {
58
-	it.session = session
59
-	it.registerTimeout = RegisterTimeout
60
-	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
61
-	registered := session.client.Registered()
62
-
63
-	it.Lock()
64
-	defer it.Unlock()
65
-	if registered {
66
-		it.state = TimerActive
67
-	} else {
68
-		it.state = TimerUnregistered
69
-	}
70
-	it.resetTimeout()
71
-}
72
-
73
-// recomputeDurations recomputes the idle and quit durations, given the client's caps.
74
-func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duration) {
75
-	totalTimeout := DefaultTotalTimeout
76
-	// if they have the resume cap, wait longer before pinging them out
77
-	// to give them a chance to resume their connection
78
-	if it.session.capabilities.Has(caps.Resume) {
79
-		totalTimeout = ResumeableTotalTimeout
80
-	}
81
-
82
-	idleTimeout = DefaultIdleTimeout
83
-	if it.session.isTor {
84
-		idleTimeout = TorIdleTimeout
85
-	}
86
-
87
-	quitTimeout = totalTimeout - idleTimeout
88
-	return
89
-}
90
-
91
-func (it *IdleTimer) Touch() {
92
-	idleTimeout, quitTimeout := it.recomputeDurations()
93
-
94
-	it.Lock()
95
-	defer it.Unlock()
96
-	it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
97
-	// a touch transitions TimerUnregistered or TimerIdle into TimerActive
98
-	if it.state != TimerDead {
99
-		it.state = TimerActive
100
-		it.resetTimeout()
101
-	}
102
-}
103
-
104
-func (it *IdleTimer) processTimeout() {
105
-	idleTimeout, quitTimeout := it.recomputeDurations()
106
-
107
-	var previousState TimerState
108
-	func() {
109
-		it.Lock()
110
-		defer it.Unlock()
111
-		it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
112
-		previousState = it.state
113
-		// TimerActive transitions to TimerIdle, all others to TimerDead
114
-		if it.state == TimerActive {
115
-			// send them a ping, give them time to respond
116
-			it.state = TimerIdle
117
-			it.resetTimeout()
118
-		} else {
119
-			it.state = TimerDead
120
-		}
121
-	}()
122
-
123
-	if previousState == TimerActive {
124
-		it.session.Ping()
125
-	} else {
126
-		it.session.client.Quit(it.quitMessage(previousState), it.session)
127
-		it.session.client.destroy(it.session)
128
-	}
129
-}
130
-
131
-// Stop stops counting idle time.
132
-func (it *IdleTimer) Stop() {
133
-	if it == nil {
134
-		return
135
-	}
136
-
137
-	it.Lock()
138
-	defer it.Unlock()
139
-	it.state = TimerDead
140
-	it.resetTimeout()
141
-}
142
-
143
-func (it *IdleTimer) resetTimeout() {
144
-	if it.timer != nil {
145
-		it.timer.Stop()
146
-	}
147
-	var nextTimeout time.Duration
148
-	switch it.state {
149
-	case TimerUnregistered:
150
-		nextTimeout = it.registerTimeout
151
-	case TimerActive:
152
-		nextTimeout = it.idleTimeout
153
-	case TimerIdle:
154
-		nextTimeout = it.quitTimeout
155
-	case TimerDead:
156
-		return
157
-	}
158
-	if it.timer != nil {
159
-		it.timer.Reset(nextTimeout)
160
-	} else {
161
-		it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
162
-	}
163
-}
164
-
165
-func (it *IdleTimer) quitMessage(state TimerState) string {
166
-	switch state {
167
-	case TimerUnregistered:
168
-		return fmt.Sprintf("Registration timeout: %v", it.registerTimeout)
169
-	case TimerIdle:
170
-		// how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout).
171
-		it.Lock()
172
-		defer it.Unlock()
173
-		return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
174
-	default:
175
-		// shouldn't happen
176
-		return ""
177
-	}
178
-}
179
-
180 10
 // BrbTimer is a timer on the client as a whole (not an individual session) for implementing
181 11
 // the BRB command and related functionality (where a client can remain online without
182 12
 // having any connected sessions).

Loading…
Cancel
Save