Re: thread resource leaks if thread terminates with uncaught exception

Jens Thiele <[email protected]> Fri, 10 Jan 2025 11:55:29 +0100
Newsgroups gmane.lisp.scheme.gauche
Message-ID <[email protected]>
Jens Thiele <[email protected]> writes:

> Jens Thiele <[email protected]> writes:
>
>> 		   (pmap (lambda(i) (http-get-body))
>> 			 (iota 400)
>> 			 :mapper (make-fully-concurrent-mapper
>> 				  0.1 'timeout)))
>
> wanted to improve the pmap method (with fully-concurrent-mapper with
> timeout):
>
> (define-method run-map ((mapper <fully-concurrent-mapper>) proc coll)
>   (let ([unique (list #f)]
>         [ts (map (^e (make-thread (^[] (proc e)))) coll)]
>         [timeout (absolute-time (~ mapper'timeout))]
>         [timeout-val (~ mapper'timeout-val)])
>     (%start-threads ts)
>     (if timeout
>       ($ map (^r (if (and (pair? r) (eq? (car r) unique))
>                    (begin (thread-terminate! (cdr r)) timeout-val)
>                    r))
>          $ map (^t (thread-join! t timeout (cons unique t))) ts)
>       (map thread-join! ts))))
>
> but I am not even sure what should happen if there are multiple uncaught
> exceptions. At the moment the first uncaught exception is raised by
> thread-join!

maybe just keep it like that

but this seems not to be good enough:

(with-module control.pmap
(define-method run-map ((mapper <fully-concurrent-mapper>) proc coll)
  (let* ([unique (list #f)]
         [ts (map (^e (make-thread (^[] (proc e)))) coll)]
	 [cleanup-ts (lambda()
		       (let loop ((ts ts))
			 (unless (null? ts)
			   (set-car! ts #f)
			   (loop (cdr ts)))))]
         [timeout (absolute-time (~ mapper'timeout))]
         [timeout-val (~ mapper'timeout-val)])
    (%start-threads ts)
    (if timeout
      (let1 results (guard (e [(uncaught-exception-condition? e)
			       ;; kill still running threads
			       (for-each thread-terminate! ts)
			       ;; otherwise we get thread died a
			       ;; lonely death with an uncaught
			       ;; exception
			       (for-each
				(lambda(t)
				  (guard
				   (e [(terminated-thread-exception? e)
				       #t]
				      [else
				       ;; #?=e
				       #t
				       ])
				   (thread-join! t)))
				ts)
			       (cleanup-ts)
			       (raise e)])
			   (map (lambda(t)
				  (thread-join! t
						timeout
						(cons unique t)))
				ts))
	(cleanup-ts)
	(map
	 (lambda(r)
	   (if (and (pair? r) (eq? (car r) unique))
	     (begin
	       (thread-terminate! (cdr r))
	       (set-cdr! r #f)
	       timeout-val)
	     r))
	 results))
      (map thread-join! ts))))
)

The complete reverse proxy example that still fails for me:

#!/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)
(use control.pmap)

(with-module control.pmap
(define-method run-map ((mapper <fully-concurrent-mapper>) proc coll)
  (let* ([unique (list #f)]
         [ts (map (^e (make-thread (^[] (proc e)))) coll)]
	 [cleanup-ts (lambda()
		       (let loop ((ts ts))
			 (unless (null? ts)
			   (set-car! ts #f)
			   (loop (cdr ts)))))]
         [timeout (absolute-time (~ mapper'timeout))]
         [timeout-val (~ mapper'timeout-val)])
    (%start-threads ts)
    (if timeout
      (let1 results (guard (e [(uncaught-exception-condition? e)
			       ;; kill still running threads
			       (for-each thread-terminate! ts)
			       ;; otherwise we get thread died a
			       ;; lonely death with an uncaught
			       ;; exception
			       (for-each
				(lambda(t)
				  (guard
				   (e [(terminated-thread-exception? e)
				       #t]
				      [else
				       ;; #?=e
				       #t
				       ])
				   (thread-join! t)))
				ts)
			       (cleanup-ts)
			       (raise e)])
			   (map (lambda(t)
				  (thread-join! t
						timeout
						(cons unique t)))
				ts))
	(cleanup-ts)
	(map
	 (lambda(r)
	   (if (and (pair? r) (eq? (car r) unique))
	     (begin
	       (thread-terminate! (cdr r))
	       (set-cdr! r #f)
	       timeout-val)
	     r))
	 results))
      (map thread-join! ts))))
)

;; 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 r (guard (e
		    [(uncaught-exception-condition? e)
		     #?=(uncaught-exception-condition-reason e)]
		    [else #?=e])
		   (pmap (lambda(i) (http-get-body))
			 (iota 400)
			 :mapper (make-fully-concurrent-mapper
				  0.1 'timeout)))
      (cond [(and (list? r) (eq? (car r) 'timeout))
	     (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"))))))]
	    [(list? r)
	     (car body)]
	    [else
	     (request-error :body r)]))))

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

running out of ideas - should I just stop it?

Jens