Repository: valyala/fasthttp
Pull request #942 (2021-01-10 17:40:54)

Allow stopping FS handler cleanup gorountine


Patch:
diff --git a/client.go b/client.go
index 56360fb..3e9fd5f 100644
--- a/client.go
+++ b/client.go
@@ -1152,7 +1152,7 @@ func clientDoDeadline(req *Request, resp *Response, deadline time.Time, c client
 	// usually continue execution on the host.
 
 	var mu sync.Mutex
-	var timedout bool
+	var timedout, responded bool
 
 	go func() {
 		reqCopy.timeout = timeout
@@ -1166,6 +1166,7 @@ func clientDoDeadline(req *Request, resp *Response, deadline time.Time, c client
 				}
 				swapRequestBody(reqCopy, req)
 				ch <- errDo
+				responded = true
 			}
 		}
 		mu.Unlock()
@@ -1181,8 +1182,12 @@ func clientDoDeadline(req *Request, resp *Response, deadline time.Time, c client
 	case <-tc.C:
 		mu.Lock()
 		{
-			timedout = true
-			err = ErrTimeout
+			if responded {
+				err = <-ch
+			} else {
+				timedout = true
+				err = ErrTimeout
+			}
 		}
 		mu.Unlock()
 	}
@@ -1317,7 +1322,7 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error)
 		req.URI().DisablePathNormalizing = true
 	}
 
-	cc, err := c.acquireConn(req.timeout)
+	cc, err := c.acquireConn(req.timeout, req.ConnectionClose())
 	if err != nil {
 		return false, err
 	}
@@ -1444,7 +1449,7 @@ func (c *HostClient) SetMaxConns(newMaxConns int) {
 	c.connsLock.Unlock()
 }
 
-func (c *HostClient) acquireConn(reqTimeout time.Duration) (cc *clientConn, err error) {
+func (c *HostClient) acquireConn(reqTimeout time.Duration, connectionClose bool) (cc *clientConn, err error) {
 	createConn := false
 	startCleaner := false
 
@@ -1459,7 +1464,7 @@ func (c *HostClient) acquireConn(reqTimeout time.Duration) (cc *clientConn, err
 		if c.connsCount < maxConns {
 			c.connsCount++
 			createConn = true
-			if !c.connsCleanerRun {
+			if !c.connsCleanerRun && !connectionClose {
 				startCleaner = true
 				c.connsCleanerRun = true
 			}
diff --git a/fs.go b/fs.go
index 77a2a24..f8d4add 100644
--- a/fs.go
+++ b/fs.go
@@ -282,6 +282,11 @@ type FS struct {
 	// FSCompressedFileSuffixes is used by default.
 	CompressedFileSuffixes map[string]string
 
+	// If CleanStop is set, the channel can be closed to stop the cleanup handlers
+	// for the FS RequestHandlers created with NewRequestHandler.
+	// NEVER close this channel while the handler is still being used!
+	CleanStop chan struct{}
+
 	once sync.Once
 	h    RequestHandler
 }
@@ -399,9 +404,29 @@ func (fs *FS) initRequestHandler() {
 
 	go func() {
 		var pendingFiles []*fsFile
+
+		clean := func() {
+			pendingFiles = h.cleanCache(pendingFiles)
+		}
+
+		if fs.CleanStop != nil {
+			t := time.NewTicker(cacheDuration / 2)
+			for {
+				select {
+				case <-t.C:
+					clean()
+				case _, stillOpen := <-fs.CleanStop:
+					// Ignore values send on the channel, only stop when it is closed.
+					if !stillOpen {
+						t.Stop()
+						return
+					}
+				}
+			}
+		}
 		for {
 			time.Sleep(cacheDuration / 2)
-			pendingFiles = h.cleanCache(pendingFiles)
+			clean()
 		}
 	}()
 
diff --git a/header.go b/header.go
index 6f6462a..dcc088e 100644
--- a/header.go
+++ b/header.go
@@ -1016,6 +1016,16 @@ func (h *RequestHeader) SetCookieBytesKV(key, value []byte) {
 }
 
 // DelClientCookie instructs the client to remove the given cookie.
+// This doesn't work for a cookie with specific domain or path,
+// you should delete it manually like:
+//
+//      c := AcquireCookie()
+//      c.SetKey(key)
+//      c.SetDomain("example.com")
+//      c.SetPath("/path")
+//      c.SetExpire(CookieExpireDelete)
+//      h.SetCookie(c)
+//      ReleaseCookie(c)
 //
 // Use DelCookie if you want just removing the cookie from response header.
 func (h *ResponseHeader) DelClientCookie(key string) {
@@ -1029,6 +1039,16 @@ func (h *ResponseHeader) DelClientCookie(key string) {
 }
 
 // DelClientCookieBytes instructs the client to remove the given cookie.
+// This doesn't work for a cookie with specific domain or path,
+// you should delete it manually like:
+//
+//      c := AcquireCookie()
+//      c.SetKey(key)
+//      c.SetDomain("example.com")
+//      c.SetPath("/path")
+//      c.SetExpire(CookieExpireDelete)
+//      h.SetCookie(c)
+//      ReleaseCookie(c)
 //
 // Use DelCookieBytes if you want just removing the cookie from response header.
 func (h *ResponseHeader) DelClientCookieBytes(key []byte) {


Test patch:
diff --git a/fs_test.go b/fs_test.go
index 8465ff8..01f8849 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -70,9 +70,13 @@ func testPathNotFound(t *testing.T, pathNotFoundFunc RequestHandler) {
 	req.SetRequestURI("http//some.url/file")
 	ctx.Init(&req, nil, TestLogger{t})
 
+	stop := make(chan struct{})
+	defer close(stop)
+
 	fs := &FS{
 		Root:         "./",
 		PathNotFound: pathNotFoundFunc,
+		CleanStop:    stop,
 	}
 	fs.NewRequestHandler()(&ctx)
 
@@ -299,9 +303,13 @@ func TestServeFileUncompressed(t *testing.T) {
 func TestFSByteRangeConcurrent(t *testing.T) {
 	t.Parallel()
 
+	stop := make(chan struct{})
+	defer close(stop)
+
 	fs := &FS{
 		Root:            ".",
 		AcceptByteRange: true,
+		CleanStop:       stop,
 	}
 	h := fs.NewRequestHandler()
 
@@ -329,9 +337,13 @@ func TestFSByteRangeConcurrent(t *testing.T) {
 func TestFSByteRangeSingleThread(t *testing.T) {
 	t.Parallel()
 
+	stop := make(chan struct{})
+	defer close(stop)
+
 	fs := &FS{
 		Root:            ".",
 		AcceptByteRange: true,
+		CleanStop:       stop,
 	}
 	h := fs.NewRequestHandler()
 
@@ -468,11 +480,15 @@ func testParseByteRangeError(t *testing.T, v string, contentLength int) {
 func TestFSCompressConcurrent(t *testing.T) {
 	// This test can't run parallel as files in / might by changed by other tests.
 
+	stop := make(chan struct{})
+	defer close(stop)
+
 	fs := &FS{
 		Root:               ".",
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()
 
@@ -501,11 +517,15 @@ func TestFSCompressConcurrent(t *testing.T) {
 func TestFSCompressSingleThread(t *testing.T) {
 	// This test can't run parallel as files in / might by changed by other tests.
 
+	stop := make(chan struct{})
+	defer close(stop)
+
 	fs := &FS{
 		Root:               ".",
 		GenerateIndexPages: true,
 		Compress:           true,
 		CompressBrotli:     true,
+		CleanStop:          stop,
 	}
 	h := fs.NewRequestHandler()