send mails asynchronously (fix #62)

This commit is contained in:
DrMaxNix 2024-02-20 18:22:13 +01:00
parent 9051f84d32
commit 17ee951df7
7 changed files with 269 additions and 77 deletions

View File

@ -58,19 +58,28 @@
// make sure session isn't locked
if(extension_loaded("session")) session_write_close();
// acquire runlock
ignore_user_abort(true);
// subscribe
if(!Newsletter::subscribe(mail_address: $mail_address, verify_key: $verify_key)){
if(Newsletter::subscribe(mail_address: $mail_address, verify_key: $verify_key)){
http_response_code(200);
echo(json_encode([
"success" => true
]));
} else {
http_response_code(200);
echo(json_encode([
"success" => false
]));
die();
}
// POSITIVE RESPONSE //
http_response_code(200);
echo(json_encode([
"success" => true
]));
// EXECUTE WORK //
// close connection
Newsletter::api_helper_http_close_connection();
// execute queued work
Newsletter::queue_work();
?>

View File

@ -59,18 +59,24 @@
if(extension_loaded("session")) session_write_close();
// unsubscribe
if(!Newsletter::unsubscribe(mail_address: $mail_address, unsubscribe_key: $unsubscribe_key)){
if(Newsletter::unsubscribe(mail_address: $mail_address, unsubscribe_key: $unsubscribe_key)){
http_response_code(200);
echo(json_encode([
"success" => true
]));
} else {
http_response_code(200);
echo(json_encode([
"success" => false
]));
die();
}
// POSITIVE RESPONSE //
http_response_code(200);
echo(json_encode([
"success" => true
]));
// EXECUTE WORK //
// close connection
Newsletter::api_helper_http_close_connection();
// execute queued work
Newsletter::queue_work();
?>

View File

@ -57,13 +57,23 @@
// make sure session isn't locked
if(extension_loaded("session")) session_write_close();
// verify
// acquire runlock
ignore_user_abort(true);
// add verify job to queue
Newsletter::verify(mail_address: $mail_address, language: $language);
// POSITIVE RESPONSE //
// positive response
http_response_code(200);
echo(json_encode([
"success" => true
]));
// EXECUTE WORK //
// close connection
Newsletter::api_helper_http_close_connection();
// execute queued work
Newsletter::queue_work();
?>

View File

@ -43,19 +43,20 @@
// make sure session isn't locked
if(extension_loaded("session")) session_write_close();
// send
if(!Newsletter::send_all(content_name: $content_name)){
http_response_code(200);
echo(json_encode([
"success" => false
]));
die();
}
// add jobs to queue
Newsletter::send_all(content_name: $content_name);
// POSITIVE RESPONSE //
// positive response
http_response_code(200);
echo(json_encode([
"success" => true
]));
// EXECUTE WORK //
// close connection
Newsletter::api_helper_http_close_connection();
// execute queued work
Newsletter::queue_work();
?>

View File

@ -67,19 +67,20 @@
// make sure session isn't locked
if(extension_loaded("session")) session_write_close();
// send
if(!Newsletter::send_one(mail_address: $mail_address, content_name: $content_name)){
http_response_code(200);
echo(json_encode([
"success" => false
]));
die();
}
// add job to queue
Newsletter::send_one(mail_address: $mail_address, content_name: $content_name);
// POSITIVE RESPONSE //
// positive response
http_response_code(200);
echo(json_encode([
"success" => true
]));
// EXECUTE WORK //
// close connection
Newsletter::api_helper_http_close_connection();
// execute queued work
Newsletter::queue_work();
?>

View File

@ -155,15 +155,15 @@
<div id="newsletter-send-one-form-feedback" class="form-feedback gone">
<div id="newsletter-send-one-form-feedback-wait" class="form-feedback-wait centertext gone">
<span class="icon spinning ti ti-loader-2"></span>
<span class="text">Sending</span>
<span class="text">Adding job to queue</span>
</div>
<div id="newsletter-send-one-form-feedback-success" class="form-feedback-success centertext gone">
<span class="icon ti ti-check"></span>
<span class="text">Successfully sent</span>
<span class="text">Job successfully queued</span>
</div>
<div id="newsletter-send-one-form-feedback-failure" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
<span class="text">Sending failed</span>
<span class="text">Queueing job failed</span>
</div>
<div id="newsletter-send-one-form-feedback-failure-no-member" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
@ -202,15 +202,15 @@
<div id="newsletter-send-all-form-feedback" class="form-feedback gone">
<div id="newsletter-send-all-form-feedback-wait" class="form-feedback-wait centertext gone">
<span class="icon spinning ti ti-loader-2"></span>
<span class="text">Sending</span>
<span class="text">Adding jobs to queue</span>
</div>
<div id="newsletter-send-all-form-feedback-success" class="form-feedback-success centertext gone">
<span class="icon ti ti-check"></span>
<span class="text">Successfully sent</span>
<span class="text">Jobs successfully queued</span>
</div>
<div id="newsletter-send-all-form-feedback-failure" class="form-feedback-failure centertext gone">
<span class="icon ti ti-x"></span>
<span class="text">Sending failed</span>
<span class="text">Queueing jobs failed</span>
</div>
</div>
</div>

View File

@ -89,17 +89,13 @@
// close database
$newsletter_db->write_close();
// maybe send verify mail
if($do_verify){
// send verify mail
$content = self::content_file_read("0000-00-00-verify");
self::send(mail_address: $mail_address, content: $content, dataset: [
"language" => $language,
"verify_key" => $verify_key
]);
} else {
// fake delay
usleep(rand(200, 2000) * 1000);
}
// release runlock
@ -272,14 +268,8 @@
// close database
$newsletter_db->write_close();
if($success){
// send welcome mail
self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
} else {
// fake delay
usleep(rand(200, 2000) * 1000);
}
// maybe send welcome mail
if($success) self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
// release runlock
ignore_user_abort($old_ignore_user_abort);
@ -399,10 +389,8 @@
*
* @param string $mail_address Member mail address.
* @param string $content_name Name of content to send.
*
* @return bool Whether sending was successful.
*/
public static function send_one(string $mail_address, string $content_name): bool {
public static function send_one(string $mail_address, string $content_name): void {
// acquire runlock
$old_ignore_user_abort = (bool)ignore_user_abort(true);
@ -410,13 +398,10 @@
$content = self::content_file_read($content_name);
// send content
$success = self::send(mail_address: $mail_address, content: $content);
self::send(mail_address: $mail_address, content: $content);
// release runlock
ignore_user_abort($old_ignore_user_abort);
// return success state
return $success;
}
@ -425,10 +410,8 @@
* Send content to all newsletter members.
*
* @param string $content_name Name of content to send.
*
* @return bool Whether sending was successful.
*/
public static function send_all(string $content_name): bool {
public static function send_all(string $content_name): void {
// LOAD LIST FROM DATABASE //
// open database
$old_ignore_user_abort = (bool)ignore_user_abort(true);
@ -446,19 +429,15 @@
$content = self::content_file_read($content_name);
// iterate over all list members
$success = true;
foreach($member_list as $one_member_mail_address => $one_member_dataset){
// send to this member
if(!self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset)) $success = false;
self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset);
}
// FINALIZE //
// release runlock
ignore_user_abort($old_ignore_user_abort);
// return success status
return $success;
}
@ -478,6 +457,11 @@
if(!is_array($db->get("list"))){
$db->set("list", []);
}
// job queue
if(!is_array($db->get("queue"))){
$db->set("queue", []);
}
}
@ -723,10 +707,8 @@
* @param array $content Mail content.
* @param ?array $dataset Previously read dataset for this member
* (this makes it possible to use one database read for multiple sends).
*
* @return bool Whether process was successful.
*/
private static function send(string $mail_address, array $content, ?array $dataset = null): bool {
private static function send(string $mail_address, array $content, ?array $dataset = null): void {
// MAYBE OBTAIN MEMBER DATASET //
if($dataset === null){
// open
@ -767,12 +749,192 @@
$body = self::content_render(content: $content, dataset: $dataset);
// SEND MAIL //
// ADD JOB //
// get subject
$subject = $content["subject"][$dataset["language"]];
// send
return self::mail_send_html(mail_address: $mail_address, subject: $subject, body: $body);
// add to queue
self::queue_job_add(type: "mail_send_html", data: [
"mail_address" => $mail_address,
"subject" => $subject,
"body" => $body,
]);
}
/**
* HELPER: Add a job to the queue.
*
* @param string $type Job type.
* @param array $data Job data.
*/
private static function queue_job_add(string $type, array $data = []): void {
// OPEN DATABASE //
// acquire runlock
$old_ignore_user_abort = (bool)ignore_user_abort(true);
// obtain handle
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
// MAYBE INITIALIZE DATABASE //
self::db_init(db: $newsletter_db);
// UPDATE DATABASE //
// get existing job ids
$used_job_id_list = array_keys($newsletter_db->get("queue"));
// generate new unique job id
$job_id = Id64::unique(length: 32, list: $used_job_id_list);
// add new job
$newsletter_db->set(["queue", $job_id], [
"type" => $type,
"data" => $data,
"retry_count" => 0,
"lock" => [
"held" => false,
"time" => 0,
],
]);
// FINALIZE //
// close database
$newsletter_db->write_close();
// release runlock
ignore_user_abort($old_ignore_user_abort);
}
/**
* HELPER: Close the http connection to the client.
*/
public static function api_helper_http_close_connection(): void {
// make sure cgi process is not killed
ignore_user_abort(true);
// make sure client stops listening
header("Connection: close");
header("Content-Encoding: none");
// current buffer is full content
header("Content-Length: " . ob_get_length());
// flush ob to cgi buffer
ob_end_flush();
// flush cgi buffer to client
flush();
}
/**
* CALLBACK: Client connection has been closed, execute queued jobs now.
*/
public static function queue_work(): void {
// INITIALIZE //
// acquire runlock
$old_ignore_user_abort = (bool)ignore_user_abort(true);
// get list of available job ids
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
$job_id_list = array_keys($newsletter_db->get("queue"));
$newsletter_db->close();
foreach($job_id_list as $one_job_id){
// TRY ACQUIRING A JOB LOCK //
// reset timeout
set_time_limit(150);
// open database
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
// make sure job still exists
$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
if($one_job_data === null){
$newsletter_db->close();
continue;
}
// check whether lock is either not held or stale
$time = time();
if($one_job_data["lock"]["held"] and $one_job_data["lock"]["time"] + 180 >= $time){
$newsletter_db->close();
continue;
}
// acquire lock
$newsletter_db->set(["queue", $one_job_id, "lock"], [
"held" => true,
"time" => $time,
]);
// close database
$newsletter_db->write_close();
// TRY EXECUTING JOB //
$success = false;
if($one_job_data["type"] === "mail_send_html"){
// `mail_send_html`
$success = self::mail_send_html(...$one_job_data["data"]);
} else {
// unknown job type
Error::warn(message: "Unknown job type", data: ["type" => $one_job_data["type"], "one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
}
// FINISH THIS JOB //
// open database
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
// make sure job still exists
$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
if($one_job_data === null){
$newsletter_db->close();
continue;
}
// maybe remove successful job
if($success){
$newsletter_db->unset(["queue", $one_job_id]);
$newsletter_db->write_close();
continue;
}
// maybe remove failed job with high retry count
if(($one_job_data["retry_count"] + 1) >= 3){
Error::warn(message: "Multiple failed job execute attempts", data: ["one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
$newsletter_db->unset(["queue", $one_job_id]);
$newsletter_db->write_close();
continue;
}
// increase retry count
$newsletter_db->set(["queue", $one_job_id, "retry_count"], $one_job_data["retry_count"] + 1);
// release job lock
$newsletter_db->set(["queue", $one_job_id, "lock"], [
"held" => false,
"time" => 0,
]);
// close database
$newsletter_db->write_close();
}
// FINALIZE //
// release runlock
ignore_user_abort($old_ignore_user_abort);
}
@ -792,6 +954,9 @@
// GENERAL SETTINGS //
// make timeout more aggressive
$phpmailer->Timeout = 30;
// we use smtp to send our mail
$phpmailer->IsSMTP();