diff mrjunejune/main.c @ 250:745fd127b2a1

[seobeo] Add bounded worker interface Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 06:23:37 -0700
parents b8b6e726964a
children 667156fcd3e3
line wrap: on
line diff
--- a/mrjunejune/main.c	Tue Aug 04 04:16:45 2026 -0700
+++ b/mrjunejune/main.c	Tue Aug 04 06:23:37 2026 -0700
@@ -16,8 +16,9 @@
 volatile sig_atomic_t stop_server = 0;
 static _Atomic uint32 counter = 0;
 static _Atomic boolean g_latex_rendering = FALSE;
+static Seobeo_Worker_Pool *g_media_worker_pool = NULL;
 
-// Media Processing Context for background threads
+// Media processing context owned by a background worker.
 typedef struct {
   int64    media_id;
   char     s3_key_original[512];
@@ -31,6 +32,7 @@
 typedef struct {
   char *input_path;
   char *output_path;
+  int result;
 } File_Converter_Config;
 
 // Server configuration (loaded from .config)
@@ -407,8 +409,8 @@
   return resp;
 }
 
-// Background thread function for media processing
-void *Simple_WebpConverter_Background(void *arg)
+// Joinable worker function for local image conversion.
+void Simple_WebpConverter_Background(void *arg)
 {
   File_Converter_Config *configuration = (File_Converter_Config *)arg;
 
@@ -416,16 +418,21 @@
   snprintf(cmd, sizeof(cmd), "ffmpeg -y -i %s -quality 80 %s 2>/tmp/error_log",
            configuration->input_path, configuration->output_path);
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Running FFmpeg: %s\n", cmd);
-  int ffmpeg_result = system(cmd);
+  configuration->result = system(cmd);
 
-  Seobeo_Log(SEOBEO_INFO, "[MEDIA] FFmpeg result: %d\n", ffmpeg_result);
-  if (ffmpeg_result != 0)
+  Seobeo_Log(
+      SEOBEO_INFO,
+      "[MEDIA] FFmpeg result: %d\n",
+      configuration->result);
+  if (configuration->result != 0)
   {
     Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: FFmpeg conversion failed\n");
-    return NULL;
+    return;
   }
-  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully converted to webp: %s\n");
-  return NULL;
+  Seobeo_Log(
+      SEOBEO_INFO,
+      "[MEDIA] Successfully converted to webp: %s\n",
+      configuration->output_path);
 }
 
 Seobeo_Request_Entry *ConvertImageToWebP(Seobeo_Request_Entry *req, Dowa_Arena *arena)
@@ -481,7 +488,10 @@
   int open_flags = O_RDWR | O_CREAT | O_EXCL;
 
   char *uuid4 = (char *)Dowa_Arena_Allocate(arena, UUID_LEN);
-  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid4);
   char *input_path = Dowa_Arena_Allocate(arena, TMP_FILE_LENGTH);;
   snprintf(input_path, TMP_FILE_LENGTH, "/tmp/%s", uuid4);
@@ -499,7 +509,10 @@
 
 
   uuid4 = (char *)Dowa_Arena_Allocate(arena, UUID_LEN);
-  seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid4);
   char *output_path = (char *)Dowa_Arena_Allocate(arena, TMP_FILE_LENGTH);;
   snprintf(output_path, TMP_FILE_LENGTH, "/tmp/%s.webp", uuid4);
@@ -523,11 +536,15 @@
   File_Converter_Config *configuration = Dowa_Arena_Allocate(arena, sizeof(File_Converter_Config));
   configuration->input_path = input_path;
   configuration->output_path = output_path;
+  configuration->result = -1;
 
-  pthread_t thread_id;
-  int thread_result = pthread_create(&thread_id, NULL, Simple_WebpConverter_Background, (void *)configuration);
-
-  if (thread_result != 0)
+  Seobeo_Thread *p_worker = Seobeo_Thread_Start(
+      Simple_WebpConverter_Background,
+      configuration,
+      NULL);
+  if (!p_worker ||
+      Seobeo_Thread_Join(p_worker) != SEOBEO_WORKER_OK ||
+      configuration->result != 0)
   {
     unlink(input_path);
     unlink(output_path);
@@ -537,14 +554,7 @@
     Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
     return resp;
   }
-  else
-  {
-    // Detach thread so it cleans up automatically when done
-    pthread_detach(thread_id);
-    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully spawned and detached thread\n");
-  }
 
-  size_t converted_size = 0;
   FILE *out_file = fopen(output_path, "rb");
   if (!out_file)
   {
@@ -607,7 +617,10 @@
   int open_flags = O_RDWR | O_CREAT | O_EXCL;
 
   char *uuid4 = (char *)Dowa_Arena_Allocate(arena, UUID_LEN);
-  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid4);
   char *input_path = Dowa_Arena_Allocate(arena, TMP_FILE_LENGTH);
   snprintf(input_path, TMP_FILE_LENGTH, "/tmp/%s", uuid4);
@@ -627,7 +640,10 @@
   write(input_fd, file_data, file_size);
   close(input_fd);
 
-  seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid4);
   char *output_path = (char *)Dowa_Arena_Allocate(arena, TMP_FILE_LENGTH);;
   snprintf(output_path, TMP_FILE_LENGTH, "/tmp/%s.mp4", uuid4);
@@ -953,7 +969,10 @@
   // Generate unique S3 key with timestamp
   char s3_key[512];
   char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN);
-  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid);
   snprintf(s3_key, sizeof(s3_key), "uploads/%s/%s", uuid, filename);
 
@@ -1336,7 +1355,10 @@
 
   // Generate UUID for this upload
   char *uuid = Dowa_Arena_Allocate(arena, UUID_LEN);
-  uint32 seed = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed, uuid);
 
   // Generate S3 keys
@@ -1423,23 +1445,22 @@
   return resp;
 }
 
-// Background thread function for media processing
-void *Media_Process_Background(void *arg)
+// Worker-pool task for S3 media processing.
+void Media_Process_Background(void *arg)
 {
   Media_Processing_Context *ctx = (Media_Processing_Context *)arg;
 
-  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Background thread started for media_id=%lld\n", (long long)ctx->media_id);
+  Seobeo_Log(SEOBEO_INFO, "[MEDIA] Background worker started for media_id=%lld\n", (long long)ctx->media_id);
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] S3 key original: %s\n", ctx->s3_key_original);
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] S3 key processed: %s\n", ctx->s3_key_processed);
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] DB path: %s\n", ctx->db_path);
 
-  // Open thread-local DB connection
+  // Open a worker-local DB connection.
   Deita_Connection *db_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, ctx->db_path);
   if (!db_conn || !Deita_Connection_Is_Open(db_conn))
   {
-    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] Thread ERROR: Failed to open database for media_id=%lld\n", (long long)ctx->media_id);
-    free(ctx);
-    return NULL;
+    Seobeo_Log(SEOBEO_ERROR, "[MEDIA] Worker ERROR: Failed to open database for media_id=%lld\n", (long long)ctx->media_id);
+    return;
   }
 
   // Update status to 'processing'
@@ -1465,8 +1486,7 @@
     Deita_Query_Execute_Update_Prepared(db_conn, update_error, 2, error_params);
     S3_Presigned_URL_Destroy(&download_url);
     Deita_Connection_Close(db_conn);
-    free(ctx);
-    return NULL;
+    return;
   }
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Generated presigned URL: %.100s...\n", download_url.url);
 
@@ -1475,8 +1495,14 @@
   char tmp_output[256];
   char *uuid_input = malloc(UUID_LEN);
   char *uuid_output = malloc(UUID_LEN);
-  uint32 seed1 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
-  uint32 seed2 = (uint32)time(NULL) ^ (uint32)pthread_self() ^ counter++;
+  uint32 seed1 =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
+  uint32 seed2 =
+      (uint32)time(NULL) ^
+      (uint32)Seobeo_Thread_Current_Id() ^
+      counter++;
   Dowa_String_UUID(seed1, uuid_input);
   Dowa_String_UUID(seed2, uuid_output);
   snprintf(tmp_input, sizeof(tmp_input), "/tmp/%s", uuid_input);
@@ -1505,8 +1531,7 @@
     if (download_resp) Seobeo_Client_Response_Destroy(download_resp);
     unlink(tmp_input);
     Deita_Connection_Close(db_conn);
-    free(ctx);
-    return NULL;
+    return;
   }
 
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully downloaded file to %s\n", tmp_input);
@@ -1535,8 +1560,7 @@
     unlink(tmp_input);
     unlink(tmp_output);
     Deita_Connection_Close(db_conn);
-    free(ctx);
-    return NULL;
+    return;
   }
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully converted to webp: %s\n", tmp_output);
 
@@ -1557,8 +1581,7 @@
     unlink(tmp_input);
     unlink(tmp_output);
     Deita_Connection_Close(db_conn);
-    free(ctx);
-    return NULL;
+    return;
   }
 
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully uploaded processed file to S3\n");
@@ -1574,9 +1597,6 @@
   unlink(tmp_input);
   unlink(tmp_output);
   Deita_Connection_Close(db_conn);
-  free(ctx);
-
-  return NULL;
 }
 
 // Media Upload API - Mark uploaded
@@ -1684,13 +1704,20 @@
 
   Seobeo_Log(SEOBEO_INFO, "[MEDIA] Content type for media_id=%lld: '%s'\n", (long long)media_id, content_type_copy);
 
-  // If content_type starts with "image/", spawn background processing thread
+  // Images are processed asynchronously by the bounded media pool.
   if (strncmp(content_type_copy, "image/", 6) == 0)
   {
-    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Detected image type, preparing to spawn background thread for media_id=%lld\n", (long long)media_id);
+    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Queueing image processing for media_id=%lld\n", (long long)media_id);
 
-    // Create context for background thread (heap allocated)
+    // The pool owns this context after a successful submission.
     Media_Processing_Context *ctx = malloc(sizeof(Media_Processing_Context));
+    if (!ctx)
+    {
+      Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+      Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+      Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Unable to allocate media work\"}", arena);
+      return resp;
+    }
     ctx->media_id = media_id;
     strncpy(ctx->s3_key_original, s3_key_original_copy, sizeof(ctx->s3_key_original) - 1);
     strncpy(ctx->s3_key_processed, s3_key_processed_copy, sizeof(ctx->s3_key_processed) - 1);
@@ -1704,23 +1731,42 @@
     ctx->db_path[sizeof(ctx->db_path) - 1] = '\0';
     ctx->s3_config = g_s3_config;
 
-    Seobeo_Log(SEOBEO_INFO, "[MEDIA] Creating pthread for media_id=%lld\n", (long long)media_id);
-
-    // Spawn detached thread
-    pthread_t thread_id;
-    int thread_result = pthread_create(&thread_id, NULL, Media_Process_Background, ctx);
-
-    if (thread_result != 0)
+    Seobeo_Worker_Result worker_result =
+        g_media_worker_pool
+            ? Seobeo_Worker_Pool_Submit(
+                g_media_worker_pool,
+                Media_Process_Background,
+                ctx,
+                free)
+            : SEOBEO_WORKER_STOPPED;
+    if (worker_result != SEOBEO_WORKER_OK)
     {
-      Seobeo_Log(SEOBEO_ERROR, "[MEDIA] ERROR: pthread_create failed with result=%d for media_id=%lld\n", thread_result, (long long)media_id);
+      Seobeo_Log(
+          SEOBEO_ERROR,
+          "[MEDIA] Worker submission failed with result=%d for media_id=%lld\n",
+          worker_result,
+          (long long)media_id);
       free(ctx);
+      const char *update_error =
+        "UPDATE media_uploads SET status='error', error_message=?, updated_at=strftime('%s','now') WHERE id=?";
+      const char *error_params[] = {
+        "Media worker queue is unavailable",
+        media_id_str,
+      };
+      Deita_Query_Execute_Update_Prepared(
+          g_db_connection,
+          update_error,
+          2,
+          error_params);
+      Dowa_HashMap_Push_Arena(resp, "status", "503", arena);
+      Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+      Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Media worker queue is unavailable\"}", arena);
+      return resp;
     }
-    else
-    {
-      // Detach thread so it cleans up automatically when done
-      pthread_detach(thread_id);
-      Seobeo_Log(SEOBEO_INFO, "[MEDIA] Successfully spawned and detached thread for media_id=%lld\n", (long long)media_id);
-    }
+    Seobeo_Log(
+        SEOBEO_INFO,
+        "[MEDIA] Submitted media_id=%lld to the worker pool\n",
+        (long long)media_id);
   }
   else
   {
@@ -1952,6 +1998,14 @@
   // Initialize database
   init_database();
 
+  g_media_worker_pool = Seobeo_Worker_Pool_Create(2, 16);
+  if (!g_media_worker_pool)
+  {
+    Seobeo_Log(
+        SEOBEO_ERROR,
+        "[MEDIA] Unable to initialize the media worker pool\n");
+  }
+
   Seobeo_Router_Init();
 
   Seobeo_Router_Register("GET", "/", GetHomePage);
@@ -2015,4 +2069,6 @@
   if (!server_port || server_port[0] == '\0')
     server_port = "6969";
   Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
+  Seobeo_Worker_Pool_Destroy(g_media_worker_pool);
+  g_media_worker_pool = NULL;
 }