Tests about a simple python3 fastcgi runner using libfcgi and the Python-C API.
python
c
wsgi
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

responder.c 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /*
  2. * Copyright (C) 2019 Weber Yann
  3. *
  4. * This file is part of PyFCGI.
  5. *
  6. * PyFCGI is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * any later version.
  10. *
  11. * PyFCGI is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with PyFCGI. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. #include "responder.h"
  20. /**@brief Exit after closing all stuff like semaphores
  21. * @ingroup work_master_proc */
  22. static void clean_exit(int status)
  23. {
  24. if(PyFCGI_conf.context.n_wrk)
  25. {
  26. kill(PyFCGI_conf.context.pid, SIGTERM);
  27. }
  28. pyfcgi_IPC_close(IPC_WSTATE | IPC_WREQS | IPC_SEMST | IPC_SHMST);
  29. pyfcgi_IPC_destroy(IPC_WSTATE | IPC_WREQS | IPC_SEMST | IPC_SHMST);
  30. exit(status);
  31. }
  32. pid_t spawn_pool_handler()
  33. {
  34. pid_t res;
  35. struct sigaction act;
  36. act.sa_handler = pyfcgi_sighandler_drop;
  37. sigemptyset(&act.sa_mask);
  38. act.sa_flags = 0;
  39. act.sa_restorer = NULL;
  40. res = fork();
  41. if(res < 0)
  42. {
  43. pyfcgi_log(LOG_ALERT, "Failed to fork pool_handler : %s",
  44. strerror(errno));
  45. return -1;
  46. }
  47. if(!res)
  48. {
  49. if(sigaction(SIGINT, &act, NULL))
  50. {
  51. pyfcgi_log(LOG_WARNING,
  52. "Unable to sigaction SIGINT handler : %s",
  53. strerror(errno));
  54. }
  55. responder_loop();
  56. exit((unsigned char)-1);
  57. }
  58. return res;
  59. }
  60. void init_context()
  61. {
  62. PyFCGI_conf.context.pid = getpid();
  63. PyFCGI_conf.context.ppid = getppid();
  64. if(pyfcgi_IPC_create(IPC_WSTATE | IPC_WREQS | IPC_SEMST | IPC_SHMST) < 0)
  65. {
  66. pyfcgi_log(LOG_ALERT, "Pool handler process is unable to init IPC components");
  67. sleep(1);
  68. clean_exit(PYFCGI_FATAL);
  69. }
  70. if(sem_post(PyFCGI_SEM(SEM_STATS).sem) < 0)
  71. {
  72. pyfcgi_log(LOG_ALERT, "Unable to POST stat semaphore : %s",
  73. strerror(errno));
  74. clean_exit(PYFCGI_FATAL);
  75. }
  76. //Alloc workers PID array
  77. PyFCGI_conf.context.wrk_pids = malloc(
  78. sizeof(pid_t)*(PyFCGI_conf.max_wrk+1));
  79. if(!PyFCGI_conf.context.wrk_pids)
  80. {
  81. pyfcgi_log(LOG_ALERT,
  82. "Unable to allocate worker PID array : %s",
  83. strerror(errno));
  84. clean_exit(PYFCGI_FATAL);
  85. }
  86. bzero(PyFCGI_conf.context.wrk_pids,
  87. sizeof(pid_t) * (PyFCGI_conf.max_wrk + 1));
  88. // Open FCGI listen socket
  89. PyFCGI_conf.context.fcgi_socket = FCGX_OpenSocket(PyFCGI_conf.sock_path,
  90. PYFCGI_SOCK_BACKLOG);
  91. if(PyFCGI_conf.context.fcgi_socket == -1)
  92. {
  93. pyfcgi_log(LOG_ALERT, "Unable to open socket : '%s'",
  94. PyFCGI_conf.sock_path);
  95. clean_exit(PYFCGI_FATAL);
  96. }
  97. if(FCGX_Init() != 0)
  98. {
  99. pyfcgi_log(LOG_ALERT, "Unable to init libfcgi");
  100. clean_exit(PYFCGI_FATAL);
  101. }
  102. pyfcgi_log(LOG_INFO, "Listening on %s", PyFCGI_conf.sock_path);
  103. }
  104. int responder_loop()
  105. {
  106. unsigned int n_wrk, wanted_n, n;
  107. pid_t *wrk_pids;
  108. int status;
  109. pid_t ret;
  110. /**@brief poll timeout */
  111. struct timespec timeout;
  112. struct timespec idle_timeout;
  113. /**@brief watchdog timeout */
  114. struct timespec pool_timeout;
  115. time_t idle_start, busy_start;
  116. short idle, busy;
  117. struct sigaction act;
  118. char *statusstr;
  119. time_t last_update, now;
  120. act.sa_handler = pool_sighandler;
  121. sigemptyset(&act.sa_mask);
  122. sigaddset(&act.sa_mask, SIGTERM);
  123. act.sa_flags = 0;
  124. act.sa_restorer = NULL;
  125. if(sigaction(SIGTERM, &act, NULL))
  126. {
  127. pyfcgi_log(LOG_ALERT,
  128. "Sigaction error for SIGTERM pool process : %s",
  129. strerror(errno));
  130. exit(PYFCGI_FATAL);
  131. }
  132. idle_timeout.tv_sec = 0;
  133. idle_timeout.tv_nsec = 100000; //0.0001s
  134. timeout.tv_sec = 0;
  135. timeout.tv_nsec = 100000000; //0.1s
  136. idle = busy = 0;
  137. pyfcgi_logger_set_ident("Workpool");
  138. if(PyFCGI_conf.pool_timeout)
  139. {
  140. pool_timeout.tv_nsec = 0;
  141. pool_timeout.tv_sec = PyFCGI_conf.pool_timeout;
  142. pyfcgi_wd_init(pool_wd_sighandler, &pool_timeout);
  143. }
  144. pyfcgi_log(LOG_INFO, "Preparing workers");
  145. init_context();
  146. pyfcgi_wd_arm();
  147. wrk_pids = PyFCGI_conf.context.wrk_pids;
  148. PyFCGI_conf.context.n_wrk = 0;
  149. wanted_n = PyFCGI_conf.min_wrk;
  150. n_wrk = 0;
  151. // prespawning minimum worker count
  152. for(n_wrk=0; n_wrk < wanted_n; n_wrk++)
  153. {
  154. wrk_pids[n_wrk] = spawn(n_wrk);
  155. PyFCGI_conf.context.n_wrk = n_wrk;
  156. }
  157. //Wait at least for a process to be ready
  158. while(!pyfcgi_pool_idle(&idle_timeout));
  159. last_update = 0;
  160. // main loop, taking care to restart terminated workers,
  161. // spawn new one if needed, etc.
  162. while(1)
  163. {
  164. pyfcgi_wd_arm();
  165. PyFCGI_conf.context.n_wrk = n_wrk;
  166. if(last_update != (now = time(NULL)))
  167. {
  168. pyfcgi_pool_shm_update(n_wrk);
  169. last_update = now;
  170. }
  171. if( (ret = waitpid(0, &status, WNOHANG)) )
  172. {
  173. if(ret < 0)
  174. {
  175. //TODO : error
  176. }
  177. for(n=0; n<n_wrk; n++)
  178. {
  179. if(wrk_pids[n] == ret)
  180. {
  181. break;
  182. }
  183. }
  184. if(n == n_wrk)
  185. {
  186. pyfcgi_log(LOG_WARNING,
  187. "Child %d stopped but was notregistered",
  188. ret);
  189. continue;
  190. }
  191. if(WIFSIGNALED(status))
  192. {
  193. if(WTERMSIG(status) == 9)
  194. {
  195. pyfcgi_log(LOG_ALERT,
  196. "Worker[%d] get killed ! No guaranty that SEM_WSTATE is OK, exiting...",
  197. n);
  198. clean_exit(PYFCGI_WORKER_FAIL);
  199. }
  200. if(WTERMSIG(status) == 11)
  201. {
  202. pyfcgi_log(LOG_ALERT,
  203. "Worker[%d] segfault ! No guaranty that SEM_WSTATE is OK, exiting...",
  204. n);
  205. clean_exit(PYFCGI_WORKER_FAIL);
  206. }
  207. else
  208. {
  209. pyfcgi_log(LOG_ALERT,
  210. "Worker[%d] terminated by signal %s(%d)",
  211. n, strsignal(WTERMSIG(status)),
  212. WTERMSIG(status));
  213. }
  214. }
  215. if(WEXITSTATUS(status))
  216. {
  217. statusstr = status2str(WEXITSTATUS(status));
  218. pyfcgi_log((WEXITSTATUS(status)&PYFCGI_FATAL)?
  219. LOG_ALERT:LOG_WARNING,
  220. "Worker[%d] exited with status %s",
  221. n, statusstr);
  222. free(statusstr);
  223. }
  224. if(!status)
  225. {
  226. pyfcgi_log(LOG_INFO,
  227. "Worker[%d] PID %d exited normally",
  228. n, wrk_pids[n]);
  229. }
  230. // respawn on same slot
  231. pyfcgi_log(LOG_DEBUG, "respawning worker #%d", n);
  232. wrk_pids[n] = spawn(n);
  233. }
  234. // Check if the pool is idle or busy
  235. if(pyfcgi_pool_idle(&idle_timeout))
  236. {
  237. // workers idle
  238. busy = 0;
  239. if(!idle)
  240. {
  241. idle = 1;
  242. idle_start = time(NULL);
  243. }
  244. else if((time(NULL) - idle_start) > PyFCGI_conf.worker_gc_timeout &&
  245. wanted_n > PyFCGI_conf.min_wrk
  246. && n_wrk - wanted_n < 2)
  247. {
  248. wanted_n--;
  249. idle = 0;
  250. }
  251. }
  252. else
  253. {
  254. idle = 0;
  255. if(!busy)
  256. {
  257. busy = 1;
  258. busy_start = time(NULL);
  259. }
  260. else if(time(NULL) - busy_start > 0 &&
  261. wanted_n < PyFCGI_conf.max_wrk)
  262. {
  263. pyfcgi_log( LOG_DEBUG,
  264. "All workers busy, spawning a new one");
  265. n = n_wrk;
  266. n_wrk++;
  267. wanted_n = n_wrk;
  268. wrk_pids[n] = spawn(n);
  269. if(!PyFCGI_conf.worker_fast_spawn)
  270. {
  271. busy_start = time(NULL);
  272. }
  273. }
  274. }
  275. // Stopping & deleting useless childs
  276. if(wanted_n < n_wrk && idle)
  277. {
  278. busy = 0;
  279. n_wrk--;
  280. kill(wrk_pids[n_wrk], SIGTERM); // kill last worker
  281. nanosleep(&timeout, NULL);
  282. if( (ret = waitpid(wrk_pids[n_wrk], &status, WNOHANG)) < 0 )
  283. {
  284. pyfcgi_log(LOG_ERR, "Pool idle since %ds but unable to kill child %d (PID %d)",
  285. PyFCGI_conf.worker_gc_timeout,
  286. n_wrk, wrk_pids[n_wrk]);
  287. kill(wrk_pids[n_wrk], SIGKILL);
  288. }
  289. else
  290. {
  291. pyfcgi_log(LOG_INFO, "Pool idle since %ds : worker[%d](%d) killed",
  292. PyFCGI_conf.worker_gc_timeout,
  293. n_wrk, wrk_pids[n_wrk]);
  294. }
  295. wrk_pids[n_wrk] = 0;
  296. idle = 0;
  297. continue;
  298. }
  299. nanosleep(&timeout, NULL);
  300. }
  301. pyfcgi_wd_arm();
  302. //Debug wait & exit
  303. for(; n_wrk != 0; n_wrk--)
  304. {
  305. waitpid(wrk_pids[n_wrk], &status, 0);
  306. pyfcgi_log(LOG_DEBUG, "Child %d stopped with status %d",
  307. wrk_pids[n_wrk], status);
  308. PyFCGI_conf.context.n_wrk = n_wrk;
  309. }
  310. //printf("Content-Type: text/html\r\n\r\nHello world !\n");
  311. pyfcgi_wd_stop();
  312. pyfcgi_log(LOG_INFO,"Child workers stoped, stopping responder");
  313. exit(0);
  314. }
  315. pid_t spawn(int wrk_id)
  316. {
  317. pid_t res;
  318. struct timespec wd_timeout;
  319. struct sigaction act;
  320. char ident[128];
  321. act.sa_handler = worker_sighandler;
  322. sigemptyset(&act.sa_mask);
  323. act.sa_flags = 0;
  324. act.sa_restorer = NULL;
  325. res = fork();
  326. if(res == -1)
  327. {
  328. pyfcgi_log(LOG_ERR, "Fork fails for worker #%d : %s",
  329. wrk_id, strerror(errno));
  330. return -1;
  331. }
  332. else if(!res)
  333. {
  334. // Child process
  335. PyFCGI_conf.context.ppid = PyFCGI_conf.context.pid;
  336. PyFCGI_conf.context.pid = getpid();
  337. snprintf(ident, 128, "Worker%2d", wrk_id);
  338. pyfcgi_logger_set_ident(ident);
  339. // Init IPC components
  340. if(pyfcgi_IPC_init(IPC_WSTATE | IPC_WREQS) < 0)
  341. {
  342. pyfcgi_log(LOG_ALERT, "Unable to initialize semaphore when spawning process...");
  343. exit(PYFCGI_FATAL);
  344. }
  345. // Set handler for SIGINT & SIGTERM
  346. /*
  347. if(sigaction(SIGINT, &(PyFCGI_conf.context.master_old_sigint),
  348. NULL))
  349. {
  350. pyfcgi_log(LOG_ALERT,
  351. "Sigaction error for worker process when restoring SIGINT handler: %s",
  352. strerror(errno));
  353. exit(PYFCGI_FATAL);
  354. }
  355. */
  356. if(sigaction(SIGTERM, &act, NULL))
  357. {
  358. pyfcgi_log(LOG_ALERT,
  359. "Sigaction error for worker process : %s",
  360. strerror(errno));
  361. exit(PYFCGI_FATAL);
  362. }
  363. // Set watchdog
  364. if(PyFCGI_conf.worker_timeout)
  365. {
  366. wd_timeout.tv_nsec = 0;
  367. wd_timeout.tv_sec = PyFCGI_conf.worker_timeout;
  368. pyfcgi_wd_init(worker_sigalrmhandler, &wd_timeout);
  369. }
  370. if(PyFCGI_conf.pep333)
  371. {
  372. exit(work333(wrk_id));
  373. }
  374. else
  375. {
  376. exit(work(wrk_id));
  377. }
  378. }
  379. pyfcgi_IPC_init(IPC_WSTATE | IPC_WREQS | IPC_SEMST);
  380. // Sleep to avoid spawning like hell thinking all workers are
  381. // busy. Let some time to this one to go up...
  382. // TODO: find a better way to avoid spawning to max_wrk
  383. //nanosleep(&timeout, NULL);
  384. pyfcgi_log( LOG_INFO,
  385. "Worker #%d spawned with PID %d", wrk_id, res);
  386. return res;
  387. }
  388. int pyfcgi_pool_state()
  389. {
  390. int err, res;
  391. if(sem_getvalue(PyFCGI_SEM(SEM_WSTATE).sem, &res) < 0)
  392. {
  393. err = errno;
  394. pyfcgi_log(LOG_ALERT, "Unable to read WSTATE semaphore value : %s",
  395. strerror(err));
  396. clean_exit(PYFCGI_FATAL);
  397. }
  398. return res;
  399. }
  400. int pyfcgi_pool_idle(const struct timespec *timeout)
  401. {
  402. int err;
  403. struct timespec abs_timeout;
  404. if(clock_gettime(CLOCK_REALTIME_COARSE, &abs_timeout) < 0)
  405. {
  406. //clock error
  407. pyfcgi_log(LOG_WARNING, "Unable to fetch asbtime for WSTATE sem_timedwait : %s",
  408. strerror(errno));
  409. }
  410. abs_timeout.tv_sec += timeout->tv_sec;
  411. if(abs_timeout.tv_nsec + timeout->tv_nsec > 999999999)
  412. {
  413. abs_timeout.tv_nsec = abs_timeout.tv_nsec + timeout->tv_nsec - 999999999;
  414. abs_timeout.tv_sec +=1;
  415. }
  416. else
  417. {
  418. abs_timeout.tv_nsec = timeout->tv_nsec;
  419. }
  420. if(sem_timedwait(PyFCGI_SEM(SEM_WSTATE).sem, &abs_timeout) < 0)
  421. {
  422. err = errno;
  423. switch(err)
  424. {
  425. case ETIMEDOUT:
  426. case EAGAIN:
  427. return 0; //busy
  428. case EINVAL:
  429. sleep(1);
  430. return 1;
  431. default:
  432. pyfcgi_log(LOG_ALERT, "Unable to wait WSTATE sem : %s",
  433. strerror(err));
  434. clean_exit(PYFCGI_FATAL);
  435. }
  436. }
  437. if(sem_post(PyFCGI_SEM(SEM_WSTATE).sem) < 0)
  438. {
  439. pyfcgi_log(LOG_ALERT,
  440. "Unable to sempost after a sem_timedwait : %s",
  441. strerror(errno));
  442. clean_exit(PYFCGI_FATAL);
  443. }
  444. return 1; //idle
  445. }
  446. void pool_sighandler(int signum)
  447. {
  448. unsigned int i, retry;
  449. int status, ret;
  450. struct timespec req;
  451. pyfcgi_log(LOG_NOTICE, "Received signal %s, cleaning & exiting...",
  452. strsignal(signum));
  453. if(PyFCGI_conf.context.n_wrk < 1) { clean_exit(0); }
  454. for(i=0; i<PyFCGI_conf.context.n_wrk; i++)
  455. {
  456. pyfcgi_log(LOG_INFO, "Sending SIGTERM to child #%d (pid %d)",
  457. i,PyFCGI_conf.context.wrk_pids[i]);
  458. kill(PyFCGI_conf.context.wrk_pids[i], SIGTERM);
  459. }
  460. retry = i = 0;
  461. while(i<PyFCGI_conf.context.n_wrk)
  462. {
  463. ret = waitpid(PyFCGI_conf.context.wrk_pids[i], &status,
  464. WNOHANG);
  465. if(ret <= 0 && retry < 3)
  466. {
  467. retry++;
  468. req.tv_sec = 0;
  469. req.tv_nsec = 100000000; //0.1s
  470. nanosleep(&req, NULL);
  471. }
  472. else
  473. {
  474. if(retry < 3)
  475. {
  476. PyFCGI_conf.context.wrk_pids[i] = 0;
  477. }
  478. retry = 0;
  479. i++;
  480. }
  481. }
  482. for(i=0; i<PyFCGI_conf.context.n_wrk; i++)
  483. {
  484. if(PyFCGI_conf.context.wrk_pids[i])
  485. {
  486. pyfcgi_log(LOG_INFO, "Sending SIGKILL to child %d", i);
  487. kill(PyFCGI_conf.context.wrk_pids[i], SIGKILL);
  488. }
  489. }
  490. PyFCGI_conf.context.n_wrk = 0;
  491. clean_exit(0);
  492. }
  493. void pool_wd_sighandler(int signum)
  494. {
  495. unsigned int i;
  496. pyfcgi_log(LOG_ALERT, "Worker pool timeout ! Attempt to kill all childs");
  497. for(i=0; i<PyFCGI_conf.context.n_wrk; i++)
  498. {
  499. pyfcgi_log(LOG_ALERT, "Child[%d] PID %d", i, PyFCGI_conf.context.wrk_pids[i]);
  500. kill(PyFCGI_conf.context.wrk_pids[i], SIGALRM);
  501. }
  502. while(PyFCGI_conf.context.n_wrk)
  503. {
  504. kill(PyFCGI_conf.context.wrk_pids[PyFCGI_conf.context.n_wrk], SIGALRM);
  505. PyFCGI_conf.context.n_wrk--;
  506. }
  507. pyfcgi_wd_stop();
  508. kill(PyFCGI_conf.context.pid, SIGTERM);
  509. clean_exit(PYFCGI_TIMEOUT);
  510. exit(PYFCGI_TIMEOUT);
  511. }
  512. void pyfcgi_pool_shm_update(int nworker)
  513. {
  514. short retry;
  515. int err;
  516. pyfcgi_stats_shm_t *data;
  517. struct timespec req;
  518. req.tv_sec = 0;
  519. req.tv_nsec = 10000000; //0.01s
  520. retry = 0;
  521. while(1)
  522. {
  523. if(sem_trywait(PyFCGI_SEM(SEM_STATS).sem) < 0)
  524. {
  525. err = errno;
  526. if(err == EAGAIN)
  527. {
  528. if(retry >= 5)
  529. {
  530. pyfcgi_log(LOG_ALERT,
  531. "Deadlock on SEM_STATS");
  532. clean_exit(PYFCGI_FATAL);
  533. }
  534. nanosleep(&req, NULL);
  535. continue;
  536. }
  537. pyfcgi_log(LOG_ALERT,
  538. "Unable to wait stats semaphore : %s",
  539. strerror(err));
  540. clean_exit(PYFCGI_FATAL);
  541. }
  542. break;
  543. }
  544. data = (pyfcgi_stats_shm_t*)PyFCGI_conf.shm.ptr;
  545. data->nworker = nworker;
  546. err = 0;
  547. if(sem_getvalue(PyFCGI_SEM(SEM_WSTATE).sem, &(data->pool_load)) < 0)
  548. {
  549. data->pool_load = -1;
  550. pyfcgi_log(LOG_ALERT,
  551. "Unable to get semaphore value for SEM_WSTATE : ",
  552. strerror(errno));
  553. err = 1;
  554. }
  555. if(sem_post(PyFCGI_SEM(SEM_STATS).sem) < 0)
  556. {
  557. pyfcgi_log(LOG_ALERT, "Unable to post sem at shm update : %s",
  558. strerror(errno));
  559. clean_exit(PYFCGI_FATAL);
  560. }
  561. if(err)
  562. {
  563. clean_exit(PYFCGI_FATAL);
  564. }
  565. }