php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Return to Bug #52569
Patch fpm-ondemand.v11-5.3.patch revision 2011-07-14 22:38 UTC by fat@php.net
Patch fpm-ondemand.v11.patch revision 2011-07-14 22:27 UTC by fat@php.net
Patch fpm-ondemand.v10-5.3.patch revision 2011-07-10 17:49 UTC by fat@php.net
Patch fpm-ondemand.v10.patch revision 2011-07-10 17:49 UTC by fat@php.net
Patch fpm-ondemand.v9-5.3.patch revision 2011-07-09 12:30 UTC by fat@php.net
Patch fpm-ondemand.v9.patch revision 2011-07-09 12:30 UTC by fat@php.net
Patch fpm-ondemand.v8-5.3.patch revision 2011-07-09 00:22 UTC by fat@php.net
Patch fpm-ondemand.v8.patch revision 2011-07-09 00:21 UTC by fat@php.net
Patch fpm-ondemand.v7-5.3.patch revision 2011-07-05 23:12 UTC by fat@php.net
Patch fpm-ondemand.v7.patch revision 2011-07-05 23:08 UTC by fat@php.net
Patch fpm-ondemand-pm-v6 revision 2010-09-25 16:27 UTC by mplomer at gmx dot de
Patch php-fpm-ondemand-pm-v5 revision 2010-08-30 08:16 UTC by mplomer at gmx dot de
Patch fpm-ondemand.v4.patch revision 2010-08-27 06:27 UTC by fat@php.net
Patch fpm-ondemand-pm-v3 revision 2010-08-25 22:12 UTC by mplomer at gmx dot de
Patch fpm-ondemand.v2.patch.txt revision 2010-08-23 22:51 UTC by fat@php.net
Patch fpm-ondemand-pm-php53 revision 2010-08-10 18:01 UTC by mplomer at gmx dot de
Patch fpm-ondemand-pm revision 2010-08-09 20:14 UTC by fat@php.net

Patch fpm-ondemand.v9-5.3.patch for FPM related Bug #52569

Patch version 2011-07-09 12:30 UTC

Return to Bug #52569 | Download this patch
This patch is obsolete

Obsoleted by patches:

This patch renders other patches obsolete

Obsolete patches:

Patch Revisions:

Developer: fat@php.net

Index: sapi/fpm/php-fpm.conf.in
===================================================================
--- sapi/fpm/php-fpm.conf.in	(revision 312965)
+++ sapi/fpm/php-fpm.conf.in	(working copy)
@@ -67,6 +67,13 @@
 ; Default Value: system defined value
 ;rlimit_core = 0
 
+; Set the delay (in microseconds) between two consecutive events on a same
+; file descriptor.
+; Note: It only works for events on file descriptors, not on timeout.
+; Note: Set to 0 to set no delay.
+; Default: 500
+;events.delay = 0
+
 ;;;;;;;;;;;;;;;;;;;;
 ; Pool Definitions ; 
 ;;;;;;;;;;;;;;;;;;;;
@@ -135,7 +142,8 @@
 ; Possible Values:
 ;   static  - a fixed number (pm.max_children) of child processes;
 ;   dynamic - the number of child processes are set dynamically based on the
-;             following directives:
+;             following directives. With this process management, there will be
+;             always at least 1 children.
 ;             pm.max_children      - the maximum number of children that can
 ;                                    be alive at the same time.
 ;             pm.start_servers     - the number of children created on startup.
@@ -147,6 +155,12 @@
 ;                                    state (waiting to process). If the number
 ;                                    of 'idle' processes is greater than this
 ;                                    number then some children will be killed.
+;  ondemand - no children are created at startup. Children will be forked when
+;             new requests will connect. The following parameter are used:
+;             pm.max_children           - the maximum number of children that
+;                                         can be alive at the same time.
+;             pm.process_idle_timeout   - The number of seconds after which
+;                                         an idle process will be killed.
 ; Note: This value is mandatory.
 pm = dynamic
 
@@ -174,6 +188,11 @@
 ; Note: Used only when pm is set to 'dynamic'
 ; Note: Mandatory when pm is set to 'dynamic'
 ;pm.max_spare_servers = 35
+
+; The number of seconds after which an idle process will be killed.
+; Note: Used only when pm is set to 'ondemand'
+; Default Value: 10s
+;pm.process_idle_timeout = 10s;
  
 ; The number of requests each child process should execute before respawning.
 ; This can be useful to work around memory leaks in 3rd party libraries. For
Index: sapi/fpm/fpm/fpm_request.h
===================================================================
--- sapi/fpm/fpm/fpm_request.h	(revision 312965)
+++ sapi/fpm/fpm/fpm_request.h	(working copy)
@@ -17,6 +17,7 @@
 
 void fpm_request_check_timed_out(struct fpm_child_s *child, struct timeval *tv, int terminate_timeout, int slowlog_timeout);
 int fpm_request_is_idle(struct fpm_child_s *child);
+int fpm_request_last_activity(struct fpm_child_s *child, struct timeval *tv);
 
 enum fpm_request_stage_e {
 	FPM_REQUEST_ACCEPTING = 1,
Index: sapi/fpm/fpm/fpm_process_ctl.c
===================================================================
--- sapi/fpm/fpm/fpm_process_ctl.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_process_ctl.c	(working copy)
@@ -355,7 +355,24 @@
 			fpm_scoreboard_update(idle, active, cur_lq, -1, -1, -1, FPM_SCOREBOARD_ACTION_SET, wp->scoreboard);
 		}
 
+		/* this is specific to PM_STYLE_ONDEMAND */
+		if (wp->config->pm == PM_STYLE_ONDEMAND) {
+			struct timeval last, now;
 
+			zlog(ZLOG_DEBUG, "[pool %s] currently %d active children, %d spare children", wp->config->name, active, idle);
+
+			if (!last_idle_child) continue;
+
+			fpm_request_last_activity(last_idle_child, &last);
+			fpm_clock_get(&now);
+			if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) {
+				last_idle_child->idle_kill = 1;
+				fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
+			}
+
+			continue;
+		}
+
 		/* the rest is only used by PM_STYLE_DYNAMIC */
 		if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
 
@@ -472,3 +489,47 @@
 }
 /* }}} */
 
+void fpm_pctl_on_socket_accept(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
+{
+	struct fpm_worker_pool_s *wp = (struct fpm_worker_pool_s *)arg;
+	struct fpm_child_s *child;
+
+
+	if (fpm_globals.parent_pid != getpid()) {
+		/* prevent a event race condition when child process
+		 * have not set up its own event loop */
+		return;
+	}
+
+	wp->socket_event_set = 0;
+
+//	zlog(ZLOG_DEBUG, "[pool %s] heartbeat running_children=%d", wp->config->name, wp->running_children);
+
+	if (wp->running_children >= wp->config->pm_max_children) {
+		if (!wp->warn_max_children) {
+			fpm_scoreboard_update(0, 0, 0, 0, 0, 1, FPM_SCOREBOARD_ACTION_INC, wp->scoreboard);
+			zlog(ZLOG_WARNING, "[pool %s] server reached max_children setting (%d), consider raising it", wp->config->name, wp->config->pm_max_children);
+			wp->warn_max_children = 1;
+		}
+
+		return;
+	}
+
+	for (child = wp->children; child; child = child->next) {
+		/* if there is at least on idle child, it will handle the connection, stop here */
+		if (fpm_request_is_idle(child)) {
+			return;
+		}
+	}
+
+	wp->warn_max_children = 0;
+	fpm_children_make(wp, 1, 1, 1);
+
+	if (fpm_globals.is_child) {
+		return;
+	}
+
+	zlog(ZLOG_DEBUG, "[pool %s] got accept without idle child available .... I forked", wp->config->name);
+}
+/* }}} */
+
Index: sapi/fpm/fpm/fpm_status.c
===================================================================
--- sapi/fpm/fpm/fpm_status.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_status.c	(working copy)
@@ -215,7 +215,7 @@
 		now_epoch = time(NULL);
 		spprintf(&buffer, 0, syntax,
 				scoreboard.pool,
-				scoreboard.pm == PM_STYLE_STATIC ? "static" : "dynamic",
+				scoreboard.pm == PM_STYLE_STATIC ? "static" : (scoreboard.pm == PM_STYLE_DYNAMIC ? "dynamic" : "ondemand"),
 				time_buffer,
 				now_epoch - scoreboard.start_epoch,
 				scoreboard.requests,
Index: sapi/fpm/fpm/fpm_process_ctl.h
===================================================================
--- sapi/fpm/fpm/fpm_process_ctl.h	(revision 312965)
+++ sapi/fpm/fpm/fpm_process_ctl.h	(working copy)
@@ -22,6 +22,7 @@
 void fpm_pctl_kill_all(int signo);
 void fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg);
 void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev, short which, void *arg);
+void fpm_pctl_on_socket_accept(struct fpm_event_s *ev, short which, void *arg);
 int fpm_pctl_child_exited();
 int fpm_pctl_init_main();
 
Index: sapi/fpm/fpm/fpm.h
===================================================================
--- sapi/fpm/fpm/fpm.h	(revision 312965)
+++ sapi/fpm/fpm/fpm.h	(working copy)
@@ -24,6 +24,7 @@
 	int max_requests; /* for this child */
 	int is_child;
 	int test_successful;
+	long int events_delay;
 };
 
 extern struct fpm_globals_s fpm_globals;
Index: sapi/fpm/fpm/fpm_conf.c
===================================================================
--- sapi/fpm/fpm/fpm_conf.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_conf.c	(working copy)
@@ -46,12 +46,13 @@
 
 #define STR2STR(a) (a ? a : "undefined")
 #define BOOL2STR(a) (a ? "yes" : "no")
-#define PM2STR(a) (a == PM_STYLE_STATIC ? "static" : "dynamic")
+#define PM2STR(a) (a == PM_STYLE_STATIC ? "static" : (a == PM_STYLE_DYNAMIC ? "dynamic" : "ondemand"))
 #define GO(field) offsetof(struct fpm_global_config_s, field)
 #define WPO(field) offsetof(struct fpm_worker_pool_config_s, field)
 
 static int fpm_conf_load_ini_file(char *filename TSRMLS_DC);
 static char *fpm_conf_set_integer(zval *value, void **config, intptr_t offset);
+static char *fpm_conf_set_long(zval *value, void **config, intptr_t offset);
 static char *fpm_conf_set_time(zval *value, void **config, intptr_t offset);
 static char *fpm_conf_set_boolean(zval *value, void **config, intptr_t offset);
 static char *fpm_conf_set_string(zval *value, void **config, intptr_t offset);
@@ -59,7 +60,7 @@
 static char *fpm_conf_set_rlimit_core(zval *value, void **config, intptr_t offset);
 static char *fpm_conf_set_pm(zval *value, void **config, intptr_t offset);
 
-struct fpm_global_config_s fpm_global_config = { .daemonize = 1 };
+struct fpm_global_config_s fpm_global_config = { .daemonize = 1, .events_delay = 500 };
 static struct fpm_worker_pool_s *current_wp = NULL;
 static int ini_recursion = 0;
 static char *ini_filename = NULL;
@@ -76,6 +77,7 @@
 	{ "log_level",                   &fpm_conf_set_log_level,   0 },
 	{ "rlimit_files",                &fpm_conf_set_integer,     GO(rlimit_files) },
 	{ "rlimit_core",                 &fpm_conf_set_rlimit_core, GO(rlimit_core) },
+	{ "events.delay",                &fpm_conf_set_long,        GO(events_delay) },
 	{ 0, 0, 0 }
 };
 
@@ -103,6 +105,7 @@
 	{ "pm.start_servers",          &fpm_conf_set_integer,     WPO(pm_start_servers) },
 	{ "pm.min_spare_servers",      &fpm_conf_set_integer,     WPO(pm_min_spare_servers) },
 	{ "pm.max_spare_servers",      &fpm_conf_set_integer,     WPO(pm_max_spare_servers) },
+	{ "pm.process_idle_timeout",   &fpm_conf_set_time,        WPO(pm_process_idle_timeout) },
 	{ "pm.status_path",            &fpm_conf_set_string,      WPO(pm_status_path) },
 	{ "ping.path",                 &fpm_conf_set_string,      WPO(ping_path) },
 	{ "ping.response",             &fpm_conf_set_string,      WPO(ping_response) },
@@ -209,6 +212,22 @@
 }
 /* }}} */
 
+static char *fpm_conf_set_long(zval *value, void **config, intptr_t offset) /* {{{ */
+{
+	char *val = Z_STRVAL_P(value);
+	char *p;
+
+	for (p = val; *p; p++) {
+		if ( p == val && *p == '-' ) continue;
+		if (*p < '0' || *p > '9') {
+			return "is not a valid number (greater or equal than zero)";
+		}
+	}
+	* (long int *) ((char *) *config + offset) = atol(val);
+	return NULL;
+}
+/* }}} */
+
 static char *fpm_conf_set_time(zval *value, void **config, intptr_t offset) /* {{{ */
 {
 	char *val = Z_STRVAL_P(value);
@@ -308,8 +327,10 @@
 		c->pm = PM_STYLE_STATIC;
 	} else if (!strcasecmp(val, "dynamic")) {
 		c->pm = PM_STYLE_DYNAMIC;
+	} else if (!strcasecmp(val, "ondemand")) {
+		c->pm = PM_STYLE_ONDEMAND;
 	} else {
-		return "invalid process manager (static or dynamic)";
+		return "invalid process manager (static, dynamic or ondemand)";
 	}
 	return NULL;
 }
@@ -375,6 +396,7 @@
 
 	memset(wp->config, 0, sizeof(struct fpm_worker_pool_config_s));
 	wp->config->listen_backlog = FPM_BACKLOG_DEFAULT;
+	wp->config->pm_process_idle_timeout = 10; /* 10s by default */
 
 	if (!fpm_worker_all_pools) {
 		fpm_worker_all_pools = wp;
@@ -534,8 +556,8 @@
 			return -1;
 		}
 
-		if (wp->config->pm != PM_STYLE_STATIC && wp->config->pm != PM_STYLE_DYNAMIC) {
-			zlog(ZLOG_ALERT, "[pool %s] the process manager is missing (static or dynamic)", wp->config->name);
+		if (wp->config->pm != PM_STYLE_STATIC && wp->config->pm != PM_STYLE_DYNAMIC && wp->config->pm != PM_STYLE_ONDEMAND) {
+			zlog(ZLOG_ALERT, "[pool %s] the process manager is missing (static, dynamic or ondemand)", wp->config->name);
 			return -1;
 		}
 
@@ -576,7 +598,23 @@
 				zlog(ZLOG_ALERT, "[pool %s] pm.start_servers(%d) must not be less than pm.min_spare_servers(%d) and not greater than pm.max_spare_servers(%d)", wp->config->name, config->pm_start_servers, config->pm_min_spare_servers, config->pm_max_spare_servers);
 				return -1;
 			}
+		} else if (wp->config->pm == PM_STYLE_ONDEMAND) {
+			struct fpm_worker_pool_config_s *config = wp->config;
 
+			if (config->pm_process_idle_timeout < 1) {
+				zlog(ZLOG_ALERT, "[pool %s] pm.process_idle_timeout(%ds) must be greater than 0s", wp->config->name, config->pm_process_idle_timeout);
+				return -1;
+			}
+
+			if (config->listen_backlog < FPM_BACKLOG_DEFAULT) {
+				zlog(ZLOG_WARNING, "[pool %s] listen.backlog(%d) was too low for the ondemand process manager. I updated it for you to %d.", wp->config->name, config->listen_backlog, FPM_BACKLOG_DEFAULT);
+				config->listen_backlog = FPM_BACKLOG_DEFAULT;
+			}
+
+			/* certainely useless but proper */
+			config->pm_start_servers = 0;
+			config->pm_min_spare_servers = 0;
+			config->pm_max_spare_servers = 0;
 		}
 
 		if (wp->config->slowlog && *wp->config->slowlog) {
@@ -800,6 +838,17 @@
 
 	fpm_evaluate_full_path(&fpm_global_config.error_log, NULL, PHP_LOCALSTATEDIR, 0);
 
+	if (fpm_global_config.events_delay < 0 ) {
+		zlog(ZLOG_ERROR, "events.delay must be null or positive");
+		return -1;
+	}
+
+	if (fpm_global_config.events_delay > 999999) {
+		zlog(ZLOG_ERROR, "events.delay max is 999999us (less than 1s)");
+		return -1;
+	}
+	fpm_globals.events_delay = fpm_global_config.events_delay;
+
 	if (0 > fpm_stdio_open_error_log(0)) {
 		return -1;
 	}
@@ -1157,6 +1206,7 @@
 	zlog(ZLOG_NOTICE, "\temergency_restart_threshold = %d", fpm_global_config.emergency_restart_threshold);
 	zlog(ZLOG_NOTICE, "\trlimit_files = %d",                fpm_global_config.rlimit_files);
 	zlog(ZLOG_NOTICE, "\trlimit_core = %d",                 fpm_global_config.rlimit_core);
+	zlog(ZLOG_NOTICE, "\tevents.delay = %lu",               fpm_global_config.events_delay);
 	zlog(ZLOG_NOTICE, " ");
 
 	for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
@@ -1180,6 +1230,7 @@
 		zlog(ZLOG_NOTICE, "\tpm.start_servers = %d",           wp->config->pm_start_servers);
 		zlog(ZLOG_NOTICE, "\tpm.min_spare_servers = %d",       wp->config->pm_min_spare_servers);
 		zlog(ZLOG_NOTICE, "\tpm.max_spare_servers = %d",       wp->config->pm_max_spare_servers);
+		zlog(ZLOG_NOTICE, "\tpm.process_idle_timeout = %d",    wp->config->pm_process_idle_timeout);
 		zlog(ZLOG_NOTICE, "\tpm.status_path = %s",             STR2STR(wp->config->pm_status_path));
 		zlog(ZLOG_NOTICE, "\tping.path = %s",                  STR2STR(wp->config->ping_path));
 		zlog(ZLOG_NOTICE, "\tping.response = %s",              STR2STR(wp->config->ping_response));
Index: sapi/fpm/fpm/fpm_conf.h
===================================================================
--- sapi/fpm/fpm/fpm_conf.h	(revision 312965)
+++ sapi/fpm/fpm/fpm_conf.h	(working copy)
@@ -27,6 +27,7 @@
 	char *error_log;
 	int rlimit_files;
 	int rlimit_core;
+	long int events_delay;
 };
 
 extern struct fpm_global_config_s fpm_global_config;
@@ -51,6 +52,7 @@
 	int pm_start_servers;
 	int pm_min_spare_servers;
 	int pm_max_spare_servers;
+	int pm_process_idle_timeout;
 	char *ping_path;
 	char *ping_response;
 	char *access_log;
@@ -74,7 +76,8 @@
 
 enum {
 	PM_STYLE_STATIC = 1,
-	PM_STYLE_DYNAMIC = 2
+	PM_STYLE_DYNAMIC = 2,
+	PM_STYLE_ONDEMAND = 3
 };
 
 int fpm_conf_init_main(int test_conf);
Index: sapi/fpm/fpm/fpm_events.c
===================================================================
--- sapi/fpm/fpm/fpm_events.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_events.c	(working copy)
@@ -264,6 +264,7 @@
 		fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
 
 		zlog(ZLOG_DEBUG, "%zu bytes have been reserved in SHM", fpm_shm_get_size_allocated());
+		zlog(ZLOG_DEBUG, "events.delay = %lu", fpm_globals.events_delay);
 		zlog(ZLOG_NOTICE, "ready to handle connections");
 	}
 
@@ -309,6 +310,15 @@
 		i = 0;
 		q = fpm_event_queue_fd;
 		while (q && i < fpm_event_nfds_max) {
+
+			/* if the event has been triggered less than the defined delay, skip it */
+			if (fpm_globals.events_delay > 0) {
+				if (timercmp(&q->ev->timeout, &now, >)) {
+					q->ev->index = -1;
+					q = q->next;
+					continue;
+				}
+			}
 			fpm_event_ufds[i].fd = q->ev->fd;
 			fpm_event_ufds[i].events = POLLIN;
 			q->ev->index = i++;
@@ -328,6 +338,15 @@
 				if (q->ev && q->ev->index >= 0 && q->ev->index < fpm_event_nfds_max) {
 					if (q->ev->fd == fpm_event_ufds[q->ev->index].fd) {
 						if (fpm_event_ufds[q->ev->index].revents & POLLIN) {
+
+							if (fpm_globals.events_delay > 0) {
+								fpm_clock_get(&now);
+								/* set a 500µs delay before trying to retrigger the event */
+								ms.tv_sec = 0;
+								ms.tv_usec = fpm_globals.events_delay;
+								timeradd(&now, &ms, &q->ev->timeout);
+							}
+
 							fpm_event_fire(q->ev);
 							/* sanity check */
 							if (fpm_globals.parent_pid != getpid()) {
Index: sapi/fpm/fpm/fpm_worker_pool.h
===================================================================
--- sapi/fpm/fpm/fpm_worker_pool.h	(revision 312965)
+++ sapi/fpm/fpm/fpm_worker_pool.h	(working copy)
@@ -37,6 +37,10 @@
 #endif
 	struct fpm_scoreboard_s *scoreboard;
 	int log_fd;
+
+	/* for ondemand PM */
+	struct fpm_event_s *ondemand_event;
+	int socket_event_set;
 };
 
 struct fpm_worker_pool_s *fpm_worker_pool_alloc();
Index: sapi/fpm/fpm/fpm_request.c
===================================================================
--- sapi/fpm/fpm/fpm_request.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_request.c	(working copy)
@@ -276,3 +276,20 @@
 	return proc->request_stage == FPM_REQUEST_ACCEPTING;
 }
 /* }}} */
+
+int fpm_request_last_activity(struct fpm_child_s *child, struct timeval *tv) /* {{{ */
+{
+	struct fpm_scoreboard_proc_s *proc;
+
+	if (!tv) return -1;
+
+	proc = fpm_scoreboard_proc_get(child->wp->scoreboard, child->scoreboard_i);
+	if (!proc) {
+		return -1;
+	}
+
+	*tv = proc->tv;
+
+	return 1;
+}
+/* }}} */
Index: sapi/fpm/fpm/fpm_children.c
===================================================================
--- sapi/fpm/fpm/fpm_children.c	(revision 312965)
+++ sapi/fpm/fpm/fpm_children.c	(working copy)
@@ -369,6 +369,12 @@
 		} else {
 			max = wp->running_children + nb_to_spawn;
 		}
+	} else if (wp->config->pm == PM_STYLE_ONDEMAND) {
+		if (!in_event_loop) { /* starting */
+			max = 0; /* do not create any child at startup */
+		} else {
+			max = wp->running_children + nb_to_spawn;
+		}
 	} else { /* PM_STYLE_STATIC */
 		max = wp->config->pm_max_children;
 	}
@@ -412,6 +418,22 @@
 
 int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
 {
+	if (wp->config->pm == PM_STYLE_ONDEMAND) {
+		wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));
+
+		if (!wp->ondemand_event) {
+			zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name);
+			// FIXME handle crash
+			return 1;
+		}
+
+		memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));
+		fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ, fpm_pctl_on_socket_accept, wp);
+		wp->socket_event_set = 1;
+		fpm_event_add(wp->ondemand_event, 0);
+
+		return 1;
+	}
 	return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
 }
 /* }}} */
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Dec 21 16:01:28 2024 UTC