Skip to content

Commit daa5a23

Browse files
zeripath6543
andauthored
Set self-adjusting deadline for connection writing (#16068)
* Set self-adjusting deadline for connection writing In #16055 it appears that the simple 5s deadline doesn't work for large file writes. Now we can't - or at least shouldn't just set no deadline as go will happily let these connections block indefinitely. However, what seems reasonable is to set some minimum rate we expect for writing. This PR suggests the following algorithm: * Every write has a minimum timeout of 5s (adjustable at compile time.) * If there has been a previous write - then consider its previous deadline, add half of the minimum timeout + 2s per kb about to written. * If that new deadline is after the minimum timeout use that. Fix #16055 * Linearly increase timeout * Make PerWriteTimeout, PerWritePerKbTimeouts configurable Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de>
1 parent 86e2789 commit daa5a23

File tree

5 files changed

+90
-39
lines changed

5 files changed

+90
-39
lines changed

custom/conf/app.example.ini

+17-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ RUN_MODE = ; prod
5151
;REDIRECT_OTHER_PORT = false
5252
;PORT_TO_REDIRECT = 80
5353
;;
54+
;; Timeout for any write to the connection. (Set to 0 to disable all timeouts.)
55+
;PER_WRITE_TIMEOUT = 30s
56+
;;
57+
;; Timeout per Kb written to connections.
58+
;PER_WRITE_PER_KB_TIMEOUT = 30s
59+
;;
5460
;; Permission for unix socket
5561
;UNIX_SOCKET_PERMISSION = 666
5662
;;
@@ -144,6 +150,14 @@ RUN_MODE = ; prod
144150
;; Enable exposure of SSH clone URL to anonymous visitors, default is false
145151
;SSH_EXPOSE_ANONYMOUS = false
146152
;;
153+
;; Timeout for any write to ssh connections. (Set to 0 to disable all timeouts.)
154+
;; Will default to the PER_WRITE_TIMEOUT.
155+
;SSH_PER_WRITE_TIMEOUT = 30s
156+
;;
157+
;; Timeout per Kb written to ssh connections.
158+
;; Will default to the PER_WRITE_PER_KB_TIMEOUT.
159+
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
160+
;;
147161
;; Indicate whether to check minimum key size with corresponding type
148162
;MINIMUM_KEY_SIZE_CHECK = false
149163
;;
@@ -1145,8 +1159,8 @@ PATH =
11451159
;;
11461160
;; When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the path where the queue will be saved.
11471161
;; This can be overridden by `ISSUE_INDEXER_QUEUE_CONN_STR`.
1148-
;; default is queues/common
1149-
;ISSUE_INDEXER_QUEUE_DIR = queues/common
1162+
;; default is queues/common
1163+
;ISSUE_INDEXER_QUEUE_DIR = queues/common
11501164
;;
11511165
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string.
11521166
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of
@@ -1201,7 +1215,7 @@ PATH =
12011215
;; default to persistable-channel
12021216
;TYPE = persistable-channel
12031217
;;
1204-
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
1218+
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
12051219
;DATADIR = queues/
12061220
;;
12071221
;; Default queue length before a channel queue will block

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a
251251
most cases you do not need to change the default value. Alter it only if
252252
your SSH server node is not the same as HTTP node. Do not set this variable
253253
if `PROTOCOL` is set to `unix`.
254+
- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to 0 to
255+
disable all timeouts.)
256+
- `PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to connections.
254257

255258
- `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
256259
- `START_SSH_SERVER`: **false**: When enabled, use the built-in SSH server.
@@ -274,6 +277,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a
274277
- `SSH_KEY_TEST_PATH`: **/tmp**: Directory to create temporary files in when testing public keys using ssh-keygen, default is the system temporary directory.
275278
- `SSH_KEYGEN_PATH`: **ssh-keygen**: Path to ssh-keygen, default is 'ssh-keygen' which means the shell is responsible for finding out which one to call.
276279
- `SSH_EXPOSE_ANONYMOUS`: **false**: Enable exposure of SSH clone URL to anonymous visitors, default is false.
280+
- `SSH_PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the SSH connections. (Set to
281+
0 to disable all timeouts.)
282+
- `SSH_PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to SSH connections.
277283
- `MINIMUM_KEY_SIZE_CHECK`: **true**: Indicate whether to check minimum key size with corresponding type.
278284

279285
- `OFFLINE_MODE`: **false**: Disables use of CDN for static files and Gravatar for profile pictures.
@@ -350,7 +356,7 @@ relation to port exhaustion.
350356
- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch.
351357
- The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility:
352358
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`.
353-
- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. (Previously this was `indexers/issues.queue`.)
359+
- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. (Previously this was `indexers/issues.queue`.)
354360
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of the form `leveldb://path/to/db?option=value&....`, and overrides `ISSUE_INDEXER_QUEUE_DIR`.
355361
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number.
356362

@@ -370,7 +376,7 @@ relation to port exhaustion.
370376
## Queue (`queue` and `queue.*`)
371377

372378
- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel` (uses a LevelDB internally), `channel`, `level`, `redis`, `dummy`
373-
- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for individual queues can be set in `queue.name` sections but will default to `DATADIR/`**`common`**. (Previously each queue would default to `DATADIR/`**`name`**.)
379+
- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for individual queues can be set in `queue.name` sections but will default to `DATADIR/`**`common`**. (Previously each queue would default to `DATADIR/`**`name`**.)
374380
- `LENGTH`: **20**: Maximal queue size before channel queues block
375381
- `BATCH_LENGTH`: **20**: Batch data before passing to the handler
376382
- `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. Options can be set using query params. Similarly LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR`

modules/graceful/server.go

+41-23
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818

1919
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/setting"
2021
)
2122

2223
var (
@@ -26,11 +27,12 @@ var (
2627
DefaultWriteTimeOut time.Duration
2728
// DefaultMaxHeaderBytes default max header bytes
2829
DefaultMaxHeaderBytes int
30+
// PerWriteWriteTimeout timeout for writes
31+
PerWriteWriteTimeout = 30 * time.Second
32+
// PerWriteWriteTimeoutKbTime is a timeout taking account of how much there is to be written
33+
PerWriteWriteTimeoutKbTime = 10 * time.Second
2934
)
3035

31-
// PerWriteWriteTimeout timeout for writes
32-
const PerWriteWriteTimeout = 5 * time.Second
33-
3436
func init() {
3537
DefaultMaxHeaderBytes = 0 // use http.DefaultMaxHeaderBytes - which currently is 1 << 20 (1MB)
3638
}
@@ -40,14 +42,16 @@ type ServeFunction = func(net.Listener) error
4042

4143
// Server represents our graceful server
4244
type Server struct {
43-
network string
44-
address string
45-
listener net.Listener
46-
wg sync.WaitGroup
47-
state state
48-
lock *sync.RWMutex
49-
BeforeBegin func(network, address string)
50-
OnShutdown func()
45+
network string
46+
address string
47+
listener net.Listener
48+
wg sync.WaitGroup
49+
state state
50+
lock *sync.RWMutex
51+
BeforeBegin func(network, address string)
52+
OnShutdown func()
53+
PerWriteTimeout time.Duration
54+
PerWritePerKbTimeout time.Duration
5155
}
5256

5357
// NewServer creates a server on network at provided address
@@ -58,11 +62,13 @@ func NewServer(network, address, name string) *Server {
5862
log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid())
5963
}
6064
srv := &Server{
61-
wg: sync.WaitGroup{},
62-
state: stateInit,
63-
lock: &sync.RWMutex{},
64-
network: network,
65-
address: address,
65+
wg: sync.WaitGroup{},
66+
state: stateInit,
67+
lock: &sync.RWMutex{},
68+
network: network,
69+
address: address,
70+
PerWriteTimeout: setting.PerWriteTimeout,
71+
PerWritePerKbTimeout: setting.PerWritePerKbTimeout,
6672
}
6773

6874
srv.BeforeBegin = func(network, addr string) {
@@ -224,9 +230,11 @@ func (wl *wrappedListener) Accept() (net.Conn, error) {
224230
closed := int32(0)
225231

226232
c = wrappedConn{
227-
Conn: c,
228-
server: wl.server,
229-
closed: &closed,
233+
Conn: c,
234+
server: wl.server,
235+
closed: &closed,
236+
perWriteTimeout: wl.server.PerWriteTimeout,
237+
perWritePerKbTimeout: wl.server.PerWritePerKbTimeout,
230238
}
231239

232240
wl.server.wg.Add(1)
@@ -249,13 +257,23 @@ func (wl *wrappedListener) File() (*os.File, error) {
249257

250258
type wrappedConn struct {
251259
net.Conn
252-
server *Server
253-
closed *int32
260+
server *Server
261+
closed *int32
262+
deadline time.Time
263+
perWriteTimeout time.Duration
264+
perWritePerKbTimeout time.Duration
254265
}
255266

256267
func (w wrappedConn) Write(p []byte) (n int, err error) {
257-
if PerWriteWriteTimeout > 0 {
258-
_ = w.Conn.SetWriteDeadline(time.Now().Add(PerWriteWriteTimeout))
268+
if w.perWriteTimeout > 0 {
269+
minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout
270+
minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout)
271+
272+
w.deadline = w.deadline.Add(minTimeout)
273+
if minDeadline.After(w.deadline) {
274+
w.deadline = minDeadline
275+
}
276+
_ = w.Conn.SetWriteDeadline(w.deadline)
259277
}
260278
return w.Conn.Write(p)
261279
}

modules/setting/setting.go

+21-11
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ var (
117117
GracefulRestartable bool
118118
GracefulHammerTime time.Duration
119119
StartupTimeout time.Duration
120+
PerWriteTimeout = 30 * time.Second
121+
PerWritePerKbTimeout = 10 * time.Second
120122
StaticURLPrefix string
121123
AbsoluteAssetURL string
122124

@@ -147,18 +149,22 @@ var (
147149
TrustedUserCAKeys []string `ini:"SSH_TRUSTED_USER_CA_KEYS"`
148150
TrustedUserCAKeysFile string `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
149151
TrustedUserCAKeysParsed []gossh.PublicKey `ini:"-"`
152+
PerWriteTimeout time.Duration `ini:"SSH_PER_WRITE_TIMEOUT"`
153+
PerWritePerKbTimeout time.Duration `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"`
150154
}{
151-
Disabled: false,
152-
StartBuiltinServer: false,
153-
Domain: "",
154-
Port: 22,
155-
ServerCiphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128"},
156-
ServerKeyExchanges: []string{"diffie-hellman-group1-sha1", "diffie-hellman-group14-sha1", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "curve25519-sha256@libssh.org"},
157-
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96"},
158-
KeygenPath: "ssh-keygen",
159-
MinimumKeySizeCheck: true,
160-
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2048},
161-
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
155+
Disabled: false,
156+
StartBuiltinServer: false,
157+
Domain: "",
158+
Port: 22,
159+
ServerCiphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128"},
160+
ServerKeyExchanges: []string{"diffie-hellman-group1-sha1", "diffie-hellman-group14-sha1", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "curve25519-sha256@libssh.org"},
161+
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96"},
162+
KeygenPath: "ssh-keygen",
163+
MinimumKeySizeCheck: true,
164+
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2048},
165+
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
166+
PerWriteTimeout: PerWriteTimeout,
167+
PerWritePerKbTimeout: PerWritePerKbTimeout,
162168
}
163169

164170
// Security settings
@@ -612,6 +618,8 @@ func NewContext() {
612618
GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true)
613619
GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second)
614620
StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second)
621+
PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
622+
PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
615623

616624
defaultAppURL := string(Protocol) + "://" + Domain
617625
if (Protocol == HTTP && HTTPPort != "80") || (Protocol == HTTPS && HTTPPort != "443") {
@@ -777,6 +785,8 @@ func NewContext() {
777785
}
778786

779787
SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
788+
SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
789+
SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
780790

781791
if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil {
782792
log.Fatal("Failed to OAuth2 settings: %v", err)

modules/ssh/ssh_graceful.go

+3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ package ssh
77
import (
88
"code.gitea.io/gitea/modules/graceful"
99
"code.gitea.io/gitea/modules/log"
10+
"code.gitea.io/gitea/modules/setting"
1011

1112
"github.com/gliderlabs/ssh"
1213
)
1314

1415
func listen(server *ssh.Server) {
1516
gracefulServer := graceful.NewServer("tcp", server.Addr, "SSH")
17+
gracefulServer.PerWriteTimeout = setting.SSH.PerWriteTimeout
18+
gracefulServer.PerWritePerKbTimeout = setting.SSH.PerWritePerKbTimeout
1619

1720
err := gracefulServer.ListenAndServe(server.Serve)
1821
if err != nil {

0 commit comments

Comments
 (0)