Re: thread resource leaks if thread terminates with uncaught exception

Jens Thiele <[email protected]> Wed, 08 Jan 2025 16:10:19 +0100
Newsgroups gmane.lisp.scheme.gauche
Message-ID <[email protected]>
Shiro Kawai <[email protected]> writes:

> A clue.  If we remove reference to each threads in the variable `threads`
> as follows, all VMs are finalized and the next gc closes opened fds.
>
> ```
>           (cond [(not body)
>                  (for-each thread-terminate! threads)
>                  (let loop ((threads threads))
>                    (unless (null? threads)
>                      (set-car! threads #f)
>                      (loop (cdr threads))))
> ```
>
> Merely nulling the variable `threads` doesn't work.  So somebody's grabbing
> the head of thread list itself, preventing them from being GC-ed.

I am still playing around with this and the code above fixes the
problems most of the time. But I also see strange effects I don't
understand with slight changes to the reverse proxy code and I can
provoke it failing after some time again using quite many threads and a
very small timeout. The reverse proxy version below just failed after 5
minutes. Anyway I still don't want to give up and return to a fork based
approach.

#!/bin/sh
#| -*- mode: scheme; coding: utf-8; -*-
#export GC_PRINT_STATS=1
exec gosh -I. -- $0 "$@"
|#
(use makiki)
(use text.html-lite)
(use rfc.http)
(use gauche.threads)
(use file.util)
(use gauche.sequence)

;; todo: linux specific
(define (num-open-files)
  (guard (e
	  [else
	   +nan.0])
	 (length (directory-list "/proc/self/fd" :children? #t))))

(define (num-threads)
  (guard (e
	  [else
	   +nan.0])
	 (length (directory-list "/proc/self/task" :children? #t))))

(define-http-handler (GET) "/" (^[req app]
				 (receive (status headers body)
				     (http-get "localhost:8081" "/")
				   (respond/ok req body))))

(define (http-get-body)
  (receive (status headers body)
      ;; todo: detect host name and use it to force a real dns lookup
      ;; to occur?
      (http-get "localhost:8081" "/slow")
    body))

(define-http-handler (GET)
  "/timeout"
  (lambda(req app)
    (let1 threads (map (lambda(i)
			 (make-thread http-get-body))
		       (iota 400))
      (for-each thread-start! threads)
      ;;#?=(num-threads)
      (let1 timeout (absolute-time 0.01)
	(let1 body
	    (guard (e
		    [(uncaught-exception-condition? e)
		     #?=(uncaught-exception-condition-reason e)]
		    [else #?=e])
		   (thread-join! (car threads) timeout #f))
	  (for-each
	   (lambda(t)
	     (guard (e
		     [(uncaught-exception-condition? e)
		      #?=(uncaught-exception-condition-reason e)]
		     [else #?=e])
		    (thread-join! t timeout #f)))
	   (cdr threads))
	  (cond [(not body)
		 ;; note: we don't need force here
		 (for-each thread-terminate! threads)
		 ;; otherwise we get thread died a lonely death with
		 ;; an uncaught exception in the error case
		 (for-each-with-index
		  (lambda(i t)
		    (guard (e [(terminated-thread-exception? e)
			       #t]
			      [else
			       #?=(list i e)])
			   (thread-join! t)))
		  threads)
		 ;; help gc cleanup (this fixes the problem? at least
		 ;; if the timeout isn't too small)
		 (let loop ((threads threads))
                   (unless (null? threads)
                     (set-car! threads #f)
                     (loop (cdr threads))))
		 ;;#?=(num-threads)
		 (respond/ok req
			     (list
			      (html-doctype)
			      (html:html
			       (html:head (html:title "timeout"))
			       (html:body
				(html:p "timeout")
				(html:p
				 (string-append
				  (x->string (num-open-files))
				  " open files"))))))]
		[else
		 body]))))))

(define (main args)
  #?=(sys-getpid)
  (debug-print-width 4000)
  (start-http-server :port 8080 :error-log #t)
  0)