Unable to do concurent threaded FastCGI

Just starting out? Need help? Post your questions and find answers here.
User avatar
Azias
New User
New User
Posts: 9
Joined: Wed Oct 04, 2017 5:59 pm
Location: Belgium / Luxembourg
Contact:

Unable to do concurent threaded FastCGI

Post by Azias »

Hello,

I've recently tried playing around with the standard FastCGI APIs for a project, but it looks like I can't get it to work in "threaded" mode where multiple threads can handle long requests simultaneously.

Image

When I start X threads starting from a port Y that increments for each new thread, I can only connect via the last port X + Y.
I can see it cycle through all the threads as """expected""" for each request, but it only does so for a single request at once, across all threads, until I call `FinishFastCGIRequest` or `WaitFastCGIRequest`.

Is there some hidden globally shared Mutex for the FastCGI APIs, or am I missing something here ?

-----------

If you want to test on your side, you can use the following code and Docker stack to setup a basic unsecure nginx server.
It should work just fine on Windows.

CgiServerTest.pb

Code: Select all

;- Compiler directives & imports

EnableExplicit

CompilerIf #PB_Compiler_Thread = 0
	CompilerError "Required: Thread-safe compiler flag."
CompilerEndIf


;- Constants & Structures

#WorkerPool_Size = 2
#WorkerPool_PostBirthDelay = 0
#WorkerPool_PostThreadCheckDelay = 10
#WorkerPool_PostFullLoopDelay = 500

#Cgi_PortPoolStart = 5600

Structure ThreadParameters
	FcgiPort.w
EndStructure


;- Macros

Macro WriteCGIConstant(Constant)
	WriteCGIString((Constant) + ": " + CGIVariable(Constant)+"<br>")
EndMacro


;- Procedures

Procedure CustomWorkerThread(*Parameters.ThreadParameters)
	
	If Not InitFastCGI(*Parameters\FcgiPort)
		Debug "Failed to call 'InitFastCGI(" + Str(*Parameters\FcgiPort) + ")' !"
		End 3
	EndIf
	
	PrintN("Started FastCGI on port " + Str(*Parameters\FcgiPort) + " !")
	
	While WaitFastCGIRequest()
		Debug "START - " + Str(*Parameters\FcgiPort)
		
		If ReadCGI()
			WriteCGIHeader(#PB_CGI_HeaderContentType, "text/html", #PB_UTF8)
			WriteCGIHeader("HeaderTest", "HeaderTestValue", #PB_UTF8 | #PB_CGI_LastHeader)
			
			WriteCGIString("<html><title>PureBasic - FastCGI</title><body>" +
			               "Hello from PureBasic FastCGI !<br>" +
			               "Actual time: <b>"+FormatDate("%hh:%ii", Date()) + "</b><br>" +
			               "Internal Port: <b>"+Str(*Parameters\FcgiPort)+ "</b><br>")
			
			WriteCGIString("</body></html>")
			
			FinishFastCGIRequest()
			
			Define LoopStart.q = ElapsedMilliseconds()
			While ElapsedMilliseconds() < LoopStart + 5000
				Delay(1)
			Wend
			
			
			;Delay(5000)
		EndIf
		
		Debug "END - " + Str(*Parameters\FcgiPort)
	Wend
EndProcedure


;- Code
;{

If Not OpenConsole()
	End 1
EndIf

If Not InitCGI()
	Debug "Failed to call 'InitCGI' !"
	End 2
EndIf

; If Not InitCGI()
; 	End 2
; EndIf

NewList WorkerPoolThreadIDs.i()
NewList WorkerPoolThreadParams.i()

Define iWorkerPool.i
Define *TmpThreadParams.ThreadParameters

; Initializing the worker lists...
For iWorkerPool = 0 To #WorkerPool_Size - 1
	InsertElement(WorkerPoolThreadIDs())
	WorkerPoolThreadIDs() = #Null
	
	InsertElement(WorkerPoolThreadParams())
	WorkerPoolThreadParams() = #Null
Next

iWorkerPool = 0
Define StartTimeTest.q = ElapsedMilliseconds()
Define Quit.b = #False
Define iThread.i = 0

; Spawning the threads, and respawning dead ones
While Quit = #False
	iThread = 0
	
	ForEach WorkerPoolThreadIDs()
		If Not IsThread(WorkerPoolThreadIDs())
			; Cleaning up some leftovers from previous threads
			SelectElement(WorkerPoolThreadParams(), ListIndex(WorkerPoolThreadIDs()))
			If WorkerPoolThreadParams()
				FreeMemory(WorkerPoolThreadParams())
			EndIf
			
			WorkerPoolThreadParams() = AllocateMemory(SizeOf(ThreadParameters))
			*TmpThreadParams = WorkerPoolThreadParams()
			;*TmpThreadParams\FcgiPort = #Cgi_PortPoolStart
			*TmpThreadParams\FcgiPort = #Cgi_PortPoolStart + iThread
			WorkerPoolThreadIDs() = CreateThread(@CustomWorkerThread(), *TmpThreadParams)
			
			; Staggers the launch of future threads (not required !)
			CompilerIf #WorkerPool_PostBirthDelay <> 0
				Delay(#WorkerPool_PostBirthDelay)
			CompilerEndIf
		EndIf
		
		CompilerIf #WorkerPool_PostThreadCheckDelay <> 0
			Delay(#WorkerPool_PostThreadCheckDelay)
		CompilerEndIf
		
		iThread = iThread + 1
	Next
	
	CompilerIf #WorkerPool_PostFullLoopDelay <> 0
		Delay(#WorkerPool_PostFullLoopDelay)
	CompilerEndIf
Wend


; Waiting for the last threads to finish...
ForEach WorkerPoolThreadIDs()
	If IsThread(WorkerPoolThreadIDs())
		WaitThread(WorkerPoolThreadIDs())
	EndIf
Next

Define EndTimeTest.q = ElapsedMilliseconds()

; Cleaning up...
ForEach WorkerPoolThreadParams()
	If WorkerPoolThreadParams()
		FreeMemory(WorkerPoolThreadParams())
	EndIf
Next

Debug "Done !"

;}

docker-compose.yml

Code: Select all

services:
  www_test_cgi:
    container_name: www-test-cgi
    build:
      context: .
    ports:
      - 5002:80
    environment:
      - TZ=Europe/Brussels
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./htdocs:/var/www/localhost/htdocs:ro
    restart: unless-stopped
    stop_grace_period: 1s
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    logging:
        driver: "json-file"
        options:
            max-size: "16m"

Dockerfile

Code: Select all

FROM nginx:alpine

# Install fcgiwrap for FastCGI support
RUN apk add --no-cache fcgiwrap spawn-fcgi

# Create directory for FastCGI sockets
RUN mkdir -p /var/run/fcgiwrap

# Copy custom nginx configuration
#COPY nginx.conf /etc/nginx/nginx.conf
#COPY default.conf /etc/nginx/conf.d/default.conf

# Copy your FastCGI application/script
#COPY cgi-bin/ /usr/lib/cgi-bin/

# Make CGI scripts executable
#RUN chmod +x /usr/lib/cgi-bin/*

# Expose port
EXPOSE 80

# Start both nginx and fcgiwrap
CMD spawn-fcgi -s /var/run/fcgiwrap/fcgiwrap.sock -P /var/run/fcgiwrap/fcgiwrap.pid /usr/bin/fcgiwrap && \
    nginx -g "daemon off;"

nginx.conf

Code: Select all

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    
    sendfile on;
    keepalive_timeout 65;
    
    include /etc/nginx/conf.d/*.conf;
}

default.conf

Code: Select all

server {
	listen 80;
	server_name localhost;
	
	# Static files
	location / {
		root /var/www/localhost/htdocs;
		index index.html index.htm;
	}
	
	# FastCGI configuration
	location /test/ {
		fastcgi_cache off;
		
		fastcgi_param REQUEST_METHOD "TEST";
		
		#fastcgi_param test123 "Test123456";
		
		fastcgi_param HTTP_HOST phabricator.localhost1;
		fastcgi_param HTTP_HOST phabricator.localhost2;
		
		fastcgi_index index.php;
		fastcgi_param REDIRECT_STATUS	200;
		
		fastcgi_param QUERY_STRING    $query_string;
		fastcgi_param REQUEST_METHOD  $request_method;
		fastcgi_param NP_TEST_01 "Value_NP_TEST_01";
		
		# Only works with the last open port
		fastcgi_pass host.docker.internal:5601;
		
		#fastcgi_index index.php;
	}
}