diff --git a/common/framework/Mail.php b/common/framework/Mail.php
index 7502c6df9..bcf9b62a3 100644
--- a/common/framework/Mail.php
+++ b/common/framework/Mail.php
@@ -560,6 +560,13 @@ class Mail
*/
public function send(): bool
{
+ // If queue is enabled, send asynchronously.
+ if (config('queue.enabled') && !defined('RXQUEUE_CRON'))
+ {
+ Queue::addTask(self::class . '::' . 'sendAsync', $this);
+ return true;
+ }
+
// Get caller information.
$backtrace = debug_backtrace(0);
if(count($backtrace) && isset($backtrace[0]['file']))
@@ -600,6 +607,17 @@ class Mail
return $this->sent;
}
+ /**
+ * Send an email asynchronously (for Queue integration).
+ *
+ * @param self $mail
+ * @return void
+ */
+ public static function sendAsync(self $mail): void
+ {
+ $mail->send();
+ }
+
/**
* Check if the message was sent.
*
diff --git a/common/framework/Push.php b/common/framework/Push.php
index 547a2295e..bc37cd2c7 100644
--- a/common/framework/Push.php
+++ b/common/framework/Push.php
@@ -402,6 +402,13 @@ class Push
*/
public function send(): bool
{
+ // If queue is enabled, send asynchronously.
+ if (config('queue.enabled') && !defined('RXQUEUE_CRON'))
+ {
+ Queue::addTask(self::class . '::' . 'sendAsync', $this);
+ return true;
+ }
+
// Get caller information.
$backtrace = debug_backtrace(0);
if(count($backtrace) && isset($backtrace[0]['file']))
@@ -464,6 +471,17 @@ class Push
return $this->sent > 0 ? true : false;
}
+ /**
+ * Send asynchronously (for Queue integration).
+ *
+ * @param self $sms
+ * @return void
+ */
+ public static function sendAsync(self $push): void
+ {
+ $push->send();
+ }
+
/**
* Get the device token
*
diff --git a/common/framework/Queue.php b/common/framework/Queue.php
new file mode 100644
index 000000000..56410e13f
--- /dev/null
+++ b/common/framework/Queue.php
@@ -0,0 +1,302 @@
+ $class_name::getName(),
+ 'required' => $class_name::getRequiredConfig(),
+ 'optional' => $class_name::getOptionalConfig(),
+ ];
+ }
+ }
+ foreach (self::$_drivers as $driver_name => $driver)
+ {
+ if ($driver->isSupported())
+ {
+ $result[$driver_name] = [
+ 'name' => $driver->getName(),
+ 'required' => $driver->getRequiredConfig(),
+ 'optional' => $driver->getOptionalConfig(),
+ ];
+ }
+ }
+ ksort($result);
+ return $result;
+ }
+
+ /**
+ * Add a task.
+ *
+ * The handler can be in one of the following formats:
+ * - Global function, e.g. myHandler
+ * - ClassName::staticMethodName
+ * - ClassName::getInstance()->methodName
+ * - new ClassName()->methodName
+ *
+ * Once identified and/or instantiated, the handler will be passed $args
+ * and $options, in that order. Each of them must be a single object.
+ *
+ * It is strongly recommended that you write a dedicated method to handle
+ * queued tasks, rather than reusing an existing method with a potentially
+ * incompatible structure. If you must to call an existing method,
+ * you should consider writing a wrapper.
+ *
+ * Any value returned by the handler will be discarded. If you throw an
+ * exception, it may be logged, but it will not cause a fatal error.
+ *
+ * @param string $handler
+ * @param ?object $args
+ * @param ?object $options
+ * @return int
+ */
+ public static function addTask(string $handler, ?object $args = null, ?object $options = null): int
+ {
+ $driver_name = config('queue.driver');
+ if (!$driver_name)
+ {
+ throw new Exceptions\FeatureDisabled('Queue not configured');
+ }
+
+ $driver = self::getDriver($driver_name);
+ if (!$driver)
+ {
+ throw new Exceptions\FeatureDisabled('Queue not configured');
+ }
+
+ return $driver->addTask($handler, $args, $options);
+ }
+
+ /**
+ * Get the first task to execute immediately.
+ *
+ * If no tasks are pending, this method will return null.
+ * Detailed scheduling of tasks will be handled by each driver.
+ *
+ * @param int $blocking
+ * @return ?object
+ */
+ public static function getTask(int $blocking = 0): ?object
+ {
+ $driver_name = config('queue.driver');
+ if (!$driver_name)
+ {
+ throw new Exceptions\FeatureDisabled('Queue not configured');
+ }
+
+ $driver = self::getDriver($driver_name);
+ if (!$driver)
+ {
+ throw new Exceptions\FeatureDisabled('Queue not configured');
+ }
+
+ return $driver->getTask($blocking);
+ }
+
+ /**
+ * Check the process key.
+ *
+ * @param string $key
+ * @return bool
+ */
+ public static function checkKey(string $key): bool
+ {
+ return config('queue.key') === $key;
+ }
+
+ /**
+ * Process the queue.
+ *
+ * This will usually be called by a separate script, run every minute
+ * through an external scheduler such as crontab or systemd.
+ *
+ * If you are on a shared hosting service, you may also call a URL
+ * using a "web cron" service provider.
+ *
+ * @param int $timeout
+ * @return void
+ */
+ public static function process(int $timeout): void
+ {
+ // This part will run in a loop until timeout.
+ $process_start_time = microtime(true);
+ while (true)
+ {
+ // Get a task from the driver.
+ $loop_start_time = microtime(true);
+ $task = self::getTask(1);
+
+ // Wait 1 second and loop back.
+ if ($task)
+ {
+ // Find the handler for the task.
+ $task->handler = trim($task->handler, '\\()');
+ $handler = null;
+ try
+ {
+ if (preg_match('/^(?:\\\\)?([\\\\\\w]+)::(\\w+)$/', $task->handler, $matches))
+ {
+ $class_name = '\\' . $matches[1];
+ $method_name = $matches[2];
+ if (class_exists($class_name) && method_exists($class_name, $method_name))
+ {
+ $handler = [$class_name, $method_name];
+ }
+ else
+ {
+ error_log('RxQueue: task handler not found: ' . $task->handler);
+ }
+ }
+ elseif (preg_match('/^(?:\\\\)?([\\\\\\w]+)::(\\w+)(?:\(\))?->(\\w+)$/', $task->handler, $matches))
+ {
+ $class_name = '\\' . $matches[1];
+ $initializer_name = $matches[2];
+ $method_name = $matches[3];
+ if (class_exists($class_name) && method_exists($class_name, $initializer_name))
+ {
+ $obj = $class_name::$initializer_name();
+ if (method_exists($obj, $method_name))
+ {
+ $handler = [$obj, $method_name];
+ }
+ else
+ {
+ error_log('RxQueue: task handler not found: ' . $task->handler);
+ }
+ }
+ else
+ {
+ error_log('RxQueue: task handler not found: ' . $task->handler);
+ }
+ }
+ elseif (preg_match('/^new (?:\\\\)?([\\\\\\w]+)(?:\(\))?->(\\w+)$/', $task->handler, $matches))
+ {
+ $class_name = '\\' . $matches[1];
+ $method_name = $matches[2];
+ if (class_exists($class_name) && method_exists($class_name, $method_name))
+ {
+ $obj = new $class_name();
+ $handler = [$obj, $method_name];
+ }
+ else
+ {
+ error_log('RxQueue: task handler not found: ' . $task->handler);
+ }
+ }
+ else
+ {
+ if (function_exists('\\' . $task->handler))
+ {
+ $handler = '\\' . $task->handler;
+ }
+ else
+ {
+ error_log('RxQueue: task handler not found: ' . $task->handler);
+ }
+ }
+ }
+ catch (\Throwable $th)
+ {
+ error_log(vsprintf('RxQueue: task handler %s could not be accessed: %s in %s:%d', [
+ $task->handler,
+ get_class($th),
+ $th->getFile(),
+ $th->getLine(),
+ ]));
+ }
+
+ // Call the handler.
+ try
+ {
+ if ($handler)
+ {
+ call_user_func($handler, $task->args, $task->options);
+ }
+ }
+ catch (\Throwable $th)
+ {
+ error_log(vsprintf('RxQueue: task handler %s threw %s in %s:%d', [
+ $task->handler,
+ get_class($th),
+ $th->getFile(),
+ $th->getLine(),
+ ]));
+ }
+ }
+
+ // If the timeout is imminent, break the loop.
+ $process_elapsed_time = microtime(true) - $process_start_time;
+ if ($process_elapsed_time > $timeout - 2)
+ {
+ break;
+ }
+
+ // If there was no task, wait 1 second to make sure that the loop isn't too tight.
+ $loop_elapsed_time = microtime(true) - $loop_start_time;
+ if (!$task && $loop_elapsed_time < 1)
+ {
+ usleep(intval((1 - $loop_elapsed_time) * 1000000));
+ }
+ }
+ }
+}
diff --git a/common/framework/SMS.php b/common/framework/SMS.php
index 0d347bc83..4bb65fd60 100644
--- a/common/framework/SMS.php
+++ b/common/framework/SMS.php
@@ -511,6 +511,13 @@ class SMS
*/
public function send(): bool
{
+ // If queue is enabled, send asynchronously.
+ if (config('queue.enabled') && !defined('RXQUEUE_CRON'))
+ {
+ Queue::addTask(self::class . '::' . 'sendAsync', $this);
+ return true;
+ }
+
// Get caller information.
$backtrace = debug_backtrace(0);
if(count($backtrace) && isset($backtrace[0]['file']))
@@ -566,6 +573,17 @@ class SMS
return $this->sent;
}
+ /**
+ * Send an SMS asynchronously (for Queue integration).
+ *
+ * @param self $sms
+ * @return void
+ */
+ public static function sendAsync(self $sms): void
+ {
+ $sms->send();
+ }
+
/**
* Check if the message was sent.
*
diff --git a/common/framework/drivers/QueueInterface.php b/common/framework/drivers/QueueInterface.php
new file mode 100644
index 000000000..71d71a452
--- /dev/null
+++ b/common/framework/drivers/QueueInterface.php
@@ -0,0 +1,71 @@
+prepare('INSERT INTO task_queue (handler, args, options) VALUES (?, ?, ?)');
+ $result = $stmt->execute([$handler, serialize($args), serialize($options)]);
+ return $result ? $oDB->getInsertID() : 0;
+ }
+
+ /**
+ * Get the first task.
+ *
+ * @param int $blocking
+ * @return ?object
+ */
+ public function getTask(int $blocking = 0): ?object
+ {
+ $oDB = RFDB::getInstance();
+ $oDB->beginTransaction();
+ $stmt = $oDB->query('SELECT * FROM task_queue ORDER BY id LIMIT 1 FOR UPDATE');
+ $result = $stmt->fetchObject();
+ $stmt->closeCursor();
+
+ if ($result)
+ {
+ $stmt = $oDB->prepare('DELETE FROM task_queue WHERE id = ?');
+ $stmt->execute([$result->id]);
+ $oDB->commit();
+
+ $result->args = unserialize($result->args);
+ $result->options = unserialize($result->options);
+ return $result;
+ }
+ else
+ {
+ $oDB->commit();
+ return null;
+ }
+ }
+}
diff --git a/common/framework/drivers/queue/dummy.php b/common/framework/drivers/queue/dummy.php
new file mode 100644
index 000000000..0abb74792
--- /dev/null
+++ b/common/framework/drivers/queue/dummy.php
@@ -0,0 +1,119 @@
+_dummy_queue = (object)[
+ 'handler' => $handler,
+ 'args' => $args,
+ 'options' => $options,
+ ];
+ return 0;
+ }
+
+ /**
+ * Get the first task.
+ *
+ * @param int $blocking
+ * @return ?object
+ */
+ public function getTask(int $blocking = 0): ?object
+ {
+ $result = $this->_dummy_queue;
+ $this->_dummy_queue = null;
+ return $result;
+ }
+}
diff --git a/common/framework/drivers/queue/redis.php b/common/framework/drivers/queue/redis.php
new file mode 100644
index 000000000..58c122184
--- /dev/null
+++ b/common/framework/drivers/queue/redis.php
@@ -0,0 +1,197 @@
+connect($config['host'], $config['port'] ?? 6379);
+ if (isset($config['user']) || isset($config['pass']))
+ {
+ $auth = [];
+ if (isset($config['user']) && $config['user']) $auth[] = $config['user'];
+ if (isset($config['pass']) && $config['pass']) $auth[] = $config['pass'];
+ $test->auth(count($auth) > 1 ? $auth : $auth[0]);
+ }
+ if (isset($config['dbnum']))
+ {
+ $test->select(intval($config['dbnum']));
+ }
+ return true;
+ }
+ catch (\Throwable $th)
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param array $config
+ */
+ public function __construct(array $config)
+ {
+ try
+ {
+ $this->_conn = new \Redis;
+ $this->_conn->connect($config['host'], $config['port'] ?? 6379);
+ if (isset($config['user']) || isset($config['pass']))
+ {
+ $auth = [];
+ if (isset($config['user']) && $config['user']) $auth[] = $config['user'];
+ if (isset($config['pass']) && $config['pass']) $auth[] = $config['pass'];
+ $this->_conn->auth(count($auth) > 1 ? $auth : $auth[0]);
+ }
+ if (isset($config['dbnum']))
+ {
+ $this->_conn->select(intval($config['dbnum']));
+ }
+ $this->_key = 'rxQueue_' . substr(hash_hmac('sha1', 'rxQueue_', config('crypto.authentication_key')), 0, 24);
+ }
+ catch (\RedisException $e)
+ {
+ $this->_conn = null;
+ }
+ }
+
+ /**
+ * Add a task.
+ *
+ * @param string $handler
+ * @param ?object $args
+ * @param ?object $options
+ * @return int
+ */
+ public function addTask(string $handler, ?object $args = null, ?object $options = null): int
+ {
+ $value = serialize((object)[
+ 'handler' => $handler,
+ 'args' => $args,
+ 'options' => $options,
+ ]);
+
+ if ($this->_conn)
+ {
+ $result = $this->_conn->rPush($this->_key, $value);
+ return intval($result);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the first task.
+ *
+ * @param int $blocking
+ * @return ?object
+ */
+ public function getTask(int $blocking = 0): ?object
+ {
+ if ($this->_conn)
+ {
+ if ($blocking > 0)
+ {
+ $result = $this->_conn->blpop($this->_key, $blocking);
+ if (is_array($result) && isset($result[1]))
+ {
+ return unserialize($result[1]);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ $result = $this->_conn->lpop($this->_key);
+ if ($result)
+ {
+ return unserialize($result);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+}
diff --git a/common/scripts/cron.php b/common/scripts/cron.php
new file mode 100644
index 000000000..563f71c70
--- /dev/null
+++ b/common/scripts/cron.php
@@ -0,0 +1,91 @@
+ 1 && function_exists('pcntl_fork') && function_exists('pcntl_waitpid'))
+{
+ // This array will keep a dictionary of subprocesses.
+ $pids = [];
+
+ // The database connection must be closed before forking.
+ Rhymix\Framework\DB::getInstance()->disconnect();
+ Rhymix\Framework\Debug::disable();
+
+ // Create the required number of subprocesses.
+ for ($i = 0; $i < $process_count; $i++)
+ {
+ $pid = pcntl_fork();
+ if ($pid > 0)
+ {
+ $pids[$pid] = true;
+ usleep(200000);
+ }
+ elseif ($pid == 0)
+ {
+ Rhymix\Framework\Queue::process($timeout);
+ exit;
+ }
+ else
+ {
+ error_log('RxQueue: could not fork!');
+ exit;
+ }
+ }
+
+ // The parent process waits for its children to finish.
+ while (count($pids))
+ {
+ $pid = pcntl_waitpid(-1, $status, \WNOHANG);
+ if ($pid)
+ {
+ unset($pids[$pid]);
+ }
+ usleep(200000);
+ }
+}
+else
+{
+ Rhymix\Framework\Queue::process($timeout);
+}
diff --git a/modules/admin/conf/module.xml b/modules/admin/conf/module.xml
index 6d1481ca8..6c3b57e87 100644
--- a/modules/admin/conf/module.xml
+++ b/modules/admin/conf/module.xml
@@ -34,6 +34,8 @@
This is an experimental feature. It may not be stable depending on your hosting environment.';
+$lang->cmd_queue_enabled = 'Use Task Queue';
+$lang->cmd_queue_enabled_help = 'The task queue will stop accepting new tasks if you uncheck the above.';
+$lang->cmd_queue_driver = 'Queue Driver';
+$lang->cmd_queue_driver_help = 'Select the driver for the task queue that suits your hosting environment and website needs.
Some drivers such as Redis will need the corresponding program to be installed on the server.';
+$lang->cmd_queue_interval = 'Calling Interval';
+$lang->cmd_queue_interval_help = 'Use a scheduler such as crontab or systemd timer to call the script on a set interval.
All tasks are processed as soon as possible regardless of the interval, but a short interval means quick recovery from any error.
For web-based cron, this should not exceed the max_execution_time setting in php.ini.
The max_execution_time on this server is %d seconds.';
+$lang->cmd_queue_process_count = 'Process Count';
+$lang->cmd_queue_process_count_help = 'Use multiple processes to increase throughput. This may increase server load significantly.
Keep a value of 1 unless you have a high-performance dedicated server.
Multiprocessing is not supported when using web-based cron.';
+$lang->cmd_queue_call_script = 'Processing Script';
+$lang->cmd_queue_webcron_key = 'Webcron Auth Key';
+$lang->cmd_queue_config_keys['host'] = 'Host';
+$lang->cmd_queue_config_keys['port'] = 'Port';
+$lang->cmd_queue_config_keys['user'] = 'User';
+$lang->cmd_queue_config_keys['pass'] = 'Password';
+$lang->cmd_queue_config_keys['dbnum'] = 'DB Number';
+$lang->msg_queue_instructions['same_as_php'] = '(same as PHP)';
+$lang->msg_queue_instructions['crontab1'] = 'Log into the server as the %s account and run crontab -e to paste the following content into your crontab. (DO NOT run it as root!)
The %s directory in the example should be replaced with a path where logs can be recorded.';
+$lang->msg_queue_instructions['crontab2'] = 'If you change the calling interval below, the crontab interval must be adjusted accordingly.';
+$lang->msg_queue_instructions['webcron'] = 'Configure an external cron service to make a GET request to the following URL every minute, or following the interval set below.
Check your logs to make sure that the cron service is reaching your website.';
+$lang->msg_queue_instructions['systemd1'] = 'Put the following content in /etc/systemd/system/rhymix-queue.service';
+$lang->msg_queue_instructions['systemd2'] = 'Put the following content in /etc/systemd/system/rhymix-queue.timer';
+$lang->msg_queue_instructions['systemd3'] = 'Execute the following commands to enable the timer, and monitor your journal to make sure that it is operating as scheduled.';
+$lang->msg_queue_driver_not_found = 'The selected task queue driver is not supported on this server.';
+$lang->msg_queue_driver_not_usable = 'The selected task queue driver failed to initialize using the configuration values you entered.';
+$lang->msg_queue_driver_cannot_be_dummy = 'In otder to use the task queue, you must select a driver other than "Not use"';
+$lang->msg_queue_invalid_config = 'Missing or invalid configuration for the selected queue driver.';
+$lang->msg_queue_invalid_interval = 'The calling interval must be between 1 and 10 minutes.';
+$lang->msg_queue_invalid_process_count = 'The process count must be between 1 and 10.';
+$lang->msg_queue_invalid_key = 'The webcron auth key must be at least 16 characters long, and only consist of alphanumeric characters.';
$lang->autoinstall = 'EasyInstall';
$lang->last_week = 'Last Week';
$lang->this_week = 'This Week';
diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php
index 0de6799a0..7fd335612 100644
--- a/modules/admin/lang/ko.php
+++ b/modules/admin/lang/ko.php
@@ -7,6 +7,7 @@ $lang->subtitle_notification = '알림 설정';
$lang->subtitle_security = '보안 설정';
$lang->subtitle_advanced = '고급 설정';
$lang->subtitle_debug = '디버그 설정';
+$lang->subtitle_queue = '비동기 작업';
$lang->subtitle_seo = 'SEO 설정';
$lang->subtitle_etc = '기타';
$lang->current_state = '현황';
@@ -278,6 +279,36 @@ $lang->og_extract_images_fallback = '사이트 대표 이미지 사용';
$lang->og_extract_hashtags = '본문에서 해시태그 추출';
$lang->og_use_nick_name = '글 작성자 이름 표시';
$lang->og_use_timestamps = '글 작성/수정 시각 표시';
+$lang->cmd_queue_description = '메일 발송, 푸시알림 등 시간이 오래 걸리거나 외부 서비스와 연동하는 작업을 비동기 처리하여 응답 속도를 개선합니다.
실험적인 기능입니다. 호스팅 환경에 따라서는 안정적으로 작동하지 않을 수도 있습니다.';
+$lang->cmd_queue_enabled = '비동기 작업 사용';
+$lang->cmd_queue_enabled_help = '체크를 해제하면 더이상 작업을 접수하지 않습니다.';
+$lang->cmd_queue_driver = '비동기 드라이버';
+$lang->cmd_queue_driver_help = '비동기 작업을 관리할 방법을 설정합니다. 호스팅 환경과 사이트의 필요에 맞추어 선택하세요.
Redis 등 일부 드라이버는 서버에 해당 기능이 설치되어 있어야 사용할 수 있습니다.';
+$lang->cmd_queue_interval = '호출 간격';
+$lang->cmd_queue_interval_help = 'crontab, systemd timer, 웹크론 등을 사용하여 일정한 주기로 스크립트를 호출해 주십시오.
모든 비동기 작업은 호출 간격과 무관하게 실시간으로 처리되나, 호출 간격이 짧으면 장애 발생시 신속하게 복구됩니다.
웹크론 사용시에는 php.ini의 실행 시간 제한을 초과하지 않는 것이 좋습니다.
이 서버의 max_execution_time은 %d초로 설정되어 있습니다.';
+$lang->cmd_queue_process_count = '프로세스 갯수';
+$lang->cmd_queue_process_count_help = '여러 개의 프로세스를 동시에 실행하여 처리 용량을 늘립니다. 서버 부하가 증가할 수 있습니다.
고성능 단독서버가 아니라면 1을 유지하시기 바랍니다.
웹크론으로 호출한 경우에는 멀티프로세싱을 지원하지 않습니다.';
+$lang->cmd_queue_call_script = '작업 처리 스크립트';
+$lang->cmd_queue_webcron_key = '웹크론 인증키';
+$lang->cmd_queue_config_keys['host'] = '호스트';
+$lang->cmd_queue_config_keys['port'] = '포트';
+$lang->cmd_queue_config_keys['user'] = '아이디';
+$lang->cmd_queue_config_keys['pass'] = '암호';
+$lang->cmd_queue_config_keys['dbnum'] = 'DB 번호';
+$lang->msg_queue_instructions['same_as_php'] = 'PHP를 실행하는 계정과 동일한';
+$lang->msg_queue_instructions['crontab1'] = '%s 계정으로 서버에 로그인하여 crontab -e 명령을 실행한 후, 아래의 내용을 붙여넣으십시오. (root 권한으로 실행하지 마십시오.)
예제의 %s 디렉토리는 로그를 기록할 수 있는 경로로 변경하여 사용하십시오.';
+$lang->msg_queue_instructions['crontab2'] = '스크립트 호출 간격을 변경할 경우, 설정에 맞추어 crontab 실행 간격도 조절하여야 합니다.';
+$lang->msg_queue_instructions['webcron'] = '아래의 URL을 1분 간격 또는 아래에서 설정한 호출 간격에 맞추어 GET으로 호출하도록 합니다.
웹크론 서비스가 방화벽이나 CDN 등에 의해 차단되지 않도록 주의하고, 정상적으로 호출되는지 서버 로그를 확인하십시오.';
+$lang->msg_queue_instructions['systemd1'] = '/etc/systemd/system/rhymix-queue.service 파일에 아래와 같은 내용을 넣습니다.';
+$lang->msg_queue_instructions['systemd2'] = '/etc/systemd/system/rhymix-queue.timer 파일에 아래와 같은 내용을 넣습니다.';
+$lang->msg_queue_instructions['systemd3'] = '아래의 명령을 실행하여 타이머를 활성화하고, 정상 작동하는지 모니터링하십시오.';
+$lang->msg_queue_driver_not_found = '이 서버에서 지원하지 않는 비동기 드라이버입니다.';
+$lang->msg_queue_driver_not_usable = '입력하신 정보로 비동기 드라이버와 연동하는 데 실패했습니다. 드라이버 설정을 확인해 주십시오.';
+$lang->msg_queue_driver_cannot_be_dummy = '비동기 작업을 사용하려면 "미사용" 이외의 드라이버를 선택해야 합니다.';
+$lang->msg_queue_invalid_config = '비동기 드라이버의 필수 설정이 누락되었습니다.';
+$lang->msg_queue_invalid_interval = '호출 간격은 1~10분 이내여야 합니다.';
+$lang->msg_queue_invalid_process_count = '프로세스 갯수는 1~10개 이내여야 합니다.';
+$lang->msg_queue_invalid_key = '웹크론 인증키는 16자 이상으로, 알파벳 대소문자와 숫자만으로 이루어져야 합니다.';
$lang->autoinstall = '쉬운 설치';
$lang->last_week = '지난주';
$lang->this_week = '이번주';
diff --git a/modules/admin/tpl/config_header.html b/modules/admin/tpl/config_header.html
index a8e7e7a1c..84e8b3a25 100644
--- a/modules/admin/tpl/config_header.html
+++ b/modules/admin/tpl/config_header.html
@@ -12,5 +12,6 @@