I made a prototype for a task processing systems.
Architecture
- Worker - is a entity that is processing one-by-one tasks for the same processor.
- Processor - (also: executable, app) executable file (language agnostic) that getting input from STDIN and returning result to STDOUT with optional logs to STDERR. Return code 0 means success, otherwise - fail.
- Task - single processing unit that contains saved file as STDIN for the processor
Briefly - worker gets file from
tasks
directory, feeds it to STDIN of executable.On success (ret code 0) - saves STDOUT to result dir and remove task file.
On fail - moves task file to
requeue
directory. It will be later moved back totasks
directory
Logic is quite straight forward and could be described by one diagram
Task states
I would like to kindly ask community to review the github code. Please advise me how to make better/faster/more idiomatic (C Linux style). I will very appreciate it.
worker.h
/**
* Definition of worker in a directory.
* Single directory should be handled by single worker
*/
typedef struct worker_t {
char *root; // root directory
char *name; // worker ID/name - base name of root directory
char *tasks_dir; // directory with files that will be fed to the application as STDIN (tasks)
char *requeue_dir; // directory where failed tasks will be added for later re-processing
char *progress_dir; // directory where stdout of application will be stored and used as result (on success)
char *complete_dir; // directory where stdout of successful tasks execution will be stored
char *bin_dir; // directory where application should be stored (first executable file)
} worker_t;
/**
* Initialize worker. Function uses malloc internally to allocate and set worker structure.
* Allocated worker should be destroyed by `worker_destroy`
* @param worker pointer to worker
* @param root_dir root directory that will be used as parent for all nested dirs (tasks, progress, ..)
*/
void worker_init(worker_t *worker, const char *root_dir);
/**
* Free all allocated resource in worker
* @param worker pre-initialized by `worker_init` instance
*/
void worker_destroy(worker_t *worker);
/**
* Prepare environment for worker: creates directories (however, root directory should be already created),
* clean old files and so on.
* @param worker initialized instance of worker
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_prepare(const worker_t *worker);
/**
* Run all queue tasks: sequentially execute `worker_run_one` for each file in tasks dir, sorted alphabetically.
* Fail-tolerant: print error if tasks failed but do not stop next executions.
* Mostly error could return if no executable found in bin-dir
* @param worker initialized instance of worker
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_run_all(const worker_t *worker);
/**
* Execute only one task identified by name (base name of task file in tasks dir).
* In case of failure - task will be automatically re-queued, in case of successful (return code 0) execution -
* stdout will be stored in complete-dir with same name as task.
* @param worker initialized instance of worker
* @param task_name name of task (base name of task file in tasks dir)
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_run_one(const worker_t *worker, const char *task_name);
/**
* Initialize inotify subsystem to watch tasks directory.
* @param worker initialized instance of worker
* @param listener reference to where inotify file descriptor will be put
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_create_listener(const worker_t *worker, int *listener);
/**
* Wait or inotify events
* @param worker initialized instance of worker
* @param listener inotify file descriptor (commonly from `worker_create_listener`)
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_listen(const worker_t *worker, int listener);
/**
* Check files in re-queue directory for expired tasks and move them back to tasks dir
* @param worker initialized instance of worker
* @param expiration_sec expiration time in seconds
* @return 0 on success, otherwise print error to stderr and return related code
*/
int worker_requeue_check(const worker_t *worker, __time_t expiration_sec);
worker.c
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include "task.h"
#include "utils.h"
void worker_init(worker_t *worker, const char *root_dir) {
char *bin_dir = filepath_join(root_dir, "app");
worker->root = strdup(root_dir);
worker->name = strdup(filepath_basename(root_dir));
worker->bin_dir = bin_dir;
worker->progress_dir = filepath_join(worker->root, "progress");
worker->requeue_dir = filepath_join(worker->root, "requeue");
worker->tasks_dir = filepath_join(worker->root, "tasks");
worker->complete_dir = filepath_join(worker->root, "complete");
}
void worker_destroy(worker_t *worker) {
if (!worker) {
return;
}
free(worker->root);
free(worker->name);
free(worker->progress_dir);
free(worker->requeue_dir);
free(worker->tasks_dir);
free(worker->complete_dir);
free(worker->bin_dir);
}
static const mode_t dir_mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
int worker_create_dirs(const worker_t *worker) {
int ret;
ret = mkdir(worker->tasks_dir, dir_mode);
if (ret != 0 && errno != EEXIST) {
perror("retask: create task dir");
return ret;
}
ret = mkdir(worker->progress_dir, dir_mode);
if (ret != 0 && errno != EEXIST) {
perror("retask: create progress dir");
return ret;
}
ret = mkdir(worker->requeue_dir, dir_mode);
if (ret != 0 && errno != EEXIST) {
perror("retask: create requeue dir");
return ret;
}
ret = mkdir(worker->complete_dir, dir_mode);
if (ret != 0 && errno != EEXIST) {
perror("retask: create complete dir");
return ret;
}
ret = mkdir(worker->bin_dir, dir_mode);
if (ret != 0 && errno != EEXIST) {
perror("retask: create bin dir");
return ret;
}
return 0;
}
int worker_clean_progress(const worker_t *worker) {
DIR *dp;
struct dirent *ep;
dp = opendir(worker->progress_dir);
if (!dp) {
fprintf(stderr, "retask: open progress dir %s: %s\n", worker->progress_dir, strerror(errno));
return errno;
}
while ((ep = readdir(dp))) {
if (ep->d_type == DT_DIR) {
continue;
}
char *path = filepath_join(worker->progress_dir, ep->d_name);
int ret = remove(path);
if (ret != 0) {
fprintf(stderr, "retask: remove progress file %s: %s\n", path, strerror(errno));
}
free(path);
}
closedir(dp);
return 0;
}
int worker_prepare(const worker_t *worker) {
int ret;
ret = worker_create_dirs(worker);
if (ret != 0) {
return ret;
}
ret = worker_clean_progress(worker);
if (ret != 0) {
return ret;
}
return 0;
}
int worker_run_all(const worker_t *worker) {
struct dirent **namelist;
int n = scandir(worker->tasks_dir, &namelist, NULL, alphasort);
if (n == -1) {
perror("retask: scandir for tasks");
return -1;
}
for (int i = 0; i < n; ++i) {
struct dirent *entry = namelist[i];
if (entry->d_type == DT_DIR) {
continue;
}
int ret = worker_run_one(worker, entry->d_name);
if (ret != 0) {
fprintf(stderr, "retask: task %s failed with code %i\n", entry->d_name, ret);
}
free(entry);
}
free(namelist);
return 0;
}
int worker_run_one(const worker_t *worker, const char *task_name) {
task_t task;
int ret = task_init(&task, worker, task_name);
if (ret != 0) {
return ret;
}
ret = task_run(&task);
task_destroy(&task);
fprintf(stderr, "retask: task %s executed with code %i\n", task_name, ret);
return ret;
}
int worker_create_listener(const worker_t *worker, int *listener) {
int fd = inotify_init();
if (fd == -1) {
fprintf(stderr, "retask: failed to add watcher on directory %s: %s\n", worker->tasks_dir, strerror(errno));
return -1;
}
int rc = inotify_add_watch(fd, worker->tasks_dir, IN_MOVED_TO | IN_CLOSE_WRITE);
if (rc == -1) {
fprintf(stderr, "retask: failed to add watcher on directory %s: %s\n", worker->tasks_dir, strerror(errno));
close(fd);
return -1;
}
*listener = fd;
return 0;
}
int worker_listen(const worker_t *worker, int listener) {
const struct inotify_event *event;
char *ptr;
char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
ssize_t len = read(listener, buf, sizeof(buf));
if (len == -1 && errno != EAGAIN) {
fprintf(stderr, "retask: failed to listen events on directory %s: %s\n", worker->tasks_dir, strerror(errno));
return -1;
}
for (ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
if (event->len)
fprintf(stderr, "retask: detected %s", event->name);
if (event->mask & IN_ISDIR)
fprintf(stderr, " [directory]\n");
else
fprintf(stderr, " [file]\n");
}
return 0;
}
int worker_requeue_check(const worker_t *worker, __time_t expiration_sec) {
DIR *dp;
struct dirent *ep;
struct stat info;
struct timeval now;
int ret = gettimeofday(&now, NULL);
if (ret != 0) {
fprintf(stderr, "retask: get current date/time: %s\n", strerror(errno));
return ret;
}
dp = opendir(worker->requeue_dir);
if (!dp) {
ret = errno;
fprintf(stderr, "retask: open requeue dir %s: %s\n", worker->requeue_dir, strerror(errno));
return ret;
}
while ((ep = readdir(dp))) {
if (ep->d_type == DT_DIR) {
continue;
}
char *src = filepath_join(worker->requeue_dir, ep->d_name);
ret = stat(src, &info);
if (ret != 0) {
fprintf(stderr, "retask: stat queued file %s: %s\n", src, strerror(errno));
free(src);
continue;
}
if (info.st_ctim.tv_sec + expiration_sec > now.tv_sec) {
continue;
}
char *dst = filepath_join(worker->tasks_dir, ep->d_name);
ret = rename(src, dst);
if (ret != 0) {
fprintf(stderr, "retask: failed move queued file %s to tasks %s: %s\n", src, dst, strerror(errno));
} else {
fprintf(stderr, "retask: re-queued file %s due to expiration time\n", src);
}
free(dst);
free(src);
}
closedir(dp);
return 0;
}
task.h
#include "worker.h"
/**
* Definition of single task inside worker. Worker should not be destroyed before task.
*/
typedef struct task_t {
char *name; // task name (basename of task file)
char *file; // task file. The file will be used as STDIN for the executable
char *progress_file; // temp file for STDOUT of the executable
char *result_file; // destination of progress file in case of successful execution (ret code 0)
char *requeue_file; // destination for task file in case of failed execution (for re-queue)
char *executable; // executable (first file marked as executable inside bin dir)
const worker_t *worker; // reference to parent worker instance
} task_t;
/**
* Initialize internal resource for task, allocate paths, detect executable in a bin dir.
* Paths will be allocated by malloc.
* @param task reference to task that will be initialized
* @param worker initialized instance of worker
* @param task_name task name (basename of file in tasks dir)
* @return 0 on success, otherwise print error to stderr and return related code
*/
int task_init(task_t *task, const worker_t *worker, const char *task_name);
/**
* Destroy (free) allocated resource (worker ref will not be touched)
* @param task initialized instance of task
*/
void task_destroy(task_t *task);
/**
* Run executable and process result files (re-queue or move to complete). This function internally runs fork and fill
* following environment variables: WORKER_ID, WORKER_ROOT_DIR, WORKER_BIN_DIR, WORKER_TASKS_DIR, TASK_ID, TASK_EXECUTABLE.
* Task file will be used as STDIN and progress file as STDOUT. STDERR will be mapped as parent process.
* @param task initialized instance of task
* @return 0 on success, otherwise print error to stderr and return related code
*/
int task_run(const task_t *task);
task.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include "../include/utils.h"
#include "../include/task.h"
int __run_app(const task_t *task, int stdin_file, int stdout_file);
int task_init(task_t *task, const worker_t *worker, const char *task_name) {
char *executable = find_executable(worker->bin_dir);
if (!executable) {
return -1;
}
task->name = strdup(task_name);
task->file = filepath_join(worker->tasks_dir, task_name);
task->progress_file = filepath_join(worker->progress_dir, task_name);
task->result_file = filepath_join(worker->complete_dir, task_name);
task->requeue_file = filepath_join(worker->requeue_dir, task_name);
task->executable = executable;
task->worker = worker;
return 0;
}
void task_destroy(task_t *task) {
free(task->file);
free(task->progress_file);
free(task->result_file);
free(task->requeue_file);
free(task->executable);
free(task->name);
}
int task_run_app(const task_t *task) {
FILE *progress = fopen(task->progress_file, "w");
if (!progress) {
fprintf(stderr, "retask: create progress file %s: %s", task->progress_file, strerror(errno));
return 1;
}
FILE *source = fopen(task->file, "r");
if (!source) {
fprintf(stderr, "retask: read task file %s: %s", task->file, strerror(errno));
fclose(progress);
return 2;
}
int ret = __run_app(task, fileno(source), fileno(progress));
fclose(source);
fclose(progress);
return ret;
}
int __run_app(const task_t *task, int stdin_file, int stdout_file) {
pid_t pid = 0;
pid = fork();
if (pid == 0) {
dup2(stdin_file, STDIN_FILENO);
dup2(stdout_file, STDOUT_FILENO);
prctl(PR_SET_PDEATHSIG, SIGTERM);
setenv("WORKER_ID", task->worker->name, 1);
setenv("WORKER_ROOT_DIR", task->worker->root, 1);
setenv("WORKER_BIN_DIR", task->worker->bin_dir, 1);
setenv("WORKER_TASKS_DIR", task->worker->tasks_dir, 1);
setenv("TASK_ID", task->name, 1);
setenv("TASK_EXECUTABLE", task->executable, 1);
execl(task->executable, task->executable, (char *) NULL);
_exit(1);
}
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid() failed");
return 1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return status;
}
int task_complete(const task_t *task, int ret_code) {
if (ret_code != 0) {
remove(task->progress_file);
return rename(task->file, task->requeue_file);
}
ret_code = rename(task->progress_file, task->result_file);
if (ret_code != 0) {
return ret_code;
}
return remove(task->file);
}
int task_run(const task_t *task) {
int ret_code = task_run_app(task);
ret_code = task_complete(task, ret_code);
return ret_code;
}
utils.h
/**
* Concat root dir and child with respect of root dir ending (with / or without).
* It is caller responsible to free returned memory.
* @param root_dir parent directory
* @param child child directory
* @return joined path
*/
char *filepath_join(const char *root_dir, const char *child);
/**
* Find first file with executable flag.
* It is caller responsible to free returned memory.
* @param bin_dir directory to scan
* @return full path on success, otherwise print error to stderr and return NULL
*/
char *find_executable(const char *bin_dir);
/**
* Get basename (last part after /). There is no memory allocation here.
* @param root_dir path to scan
* @return ref to the first symbol in root_dir var
*/
const char *filepath_basename(const char *root_dir);
utils.c
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include "../include/utils.h"
char *find_executable(const char *bin_dir) {
DIR *dirp;
struct dirent *direntp;
dirp = opendir(bin_dir);
if (dirp == NULL) {
fprintf(stderr, "can't detect executable in %s: %s\n", bin_dir, strerror(errno));
return NULL;
}
for (;;) {
direntp = readdir(dirp);
if (direntp == NULL) {
break;
}
if (direntp->d_type == DT_DIR) {
continue;
}
char *path = filepath_join(bin_dir, direntp->d_name);
if (access(path, X_OK) != -1) {
closedir(dirp);
return path;
}
free(path);
}
closedir(dirp);
fprintf(stderr, "there is no executable file in %s\n", bin_dir);
return NULL;
}
char *filepath_join(const char *root_dir, const char *child) {
size_t child_len = strlen(child);
size_t root_len = strlen(root_dir);
size_t n = root_len + child_len + 1;
char need_slash = 0;
if (root_len > 0 && root_dir[root_len - 1] != '/') {
++n;
need_slash = 1;
}
char *data = (char *) malloc(n);
memcpy(data, root_dir, root_len);
if (need_slash) {
data[root_len] = '/';
}
memcpy(&data[root_len + need_slash], child, child_len);
data[n - 1] = '\0';
return data;
}
const char *filepath_basename(const char *root_dir) {
size_t len = strlen(root_dir);
if (len == 0) {
return root_dir;
}
for (ssize_t i = len - 1; i >= 0; --i) {
if (root_dir[i] == '/') {
return &root_dir[i + 1];
}
}
return root_dir;
}
main.c
#include <unistd.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <stdio.h>
#include <wait.h>
#include <string.h>
#include <errno.h>
#include <stdnoreturn.h>
#include "worker.h"
static const long DEFAULT_REQUEUE_INTERVAL = 5;
static const char DEFAULT_LOCATION[] = ".";
static const char REQUEUE_PROCESS[] = "requeue";
noreturn void run_requeue(worker_t *worker, long requeue_interval) {
long wait_seconds = requeue_interval / 2;
if (wait_seconds < 1) {
wait_seconds = 1;
}
for (;;) {
int ret = worker_requeue_check(worker, requeue_interval);
if (ret != 0) {
fprintf(stderr, "retask: requeue failed with code %i\n", ret);
}
sleep(wait_seconds);
}
}
int run_worker(worker_t *worker) {
int ret;
int listener;
ret = worker_create_listener(worker, &listener);
if (ret != 0) {
return ret;
}
for (;;) {
ret = worker_run_all(worker);
if (ret != 0) {
break;
}
ret = worker_listen(worker, listener);
if (ret != 0) {
break;
}
}
close(listener);
return 0;
}
int launch(worker_t *worker, long requeue_interval) {
int ret = worker_prepare(worker);
if (ret != 0) {
return ret;
}
if (fork() == 0) {
// sub process to manage requeue process
size_t name_len = strlen(worker->name);
char *process_name = (char *) malloc(name_len + 1 + sizeof(REQUEUE_PROCESS));
memcpy(process_name, worker->name, name_len);
process_name[name_len] = '/';
memcpy(&process_name[name_len + 1], REQUEUE_PROCESS, sizeof(REQUEUE_PROCESS));
prctl(PR_SET_NAME, process_name, 0, 0, 0);
prctl(PR_SET_PDEATHSIG, SIGTERM);
free(process_name);
run_requeue(worker, requeue_interval);
_exit(EXIT_FAILURE);
}
return run_worker(worker);
}
void usage() {
fprintf(stderr, "Executor for tasks from filesystem\n");
fprintf(stderr, "\n");
fprintf(stderr, "Author: Baryshnikov Aleksandr <[email protected]>\n");
fprintf(stderr, "\n");
fprintf(stderr, "retask [flags]\n");
fprintf(stderr, " -c <directory> [default=.] use specified directory as work dir\n");
fprintf(stderr, " -r <seconds> [default=%li] interval for requeue\n", DEFAULT_REQUEUE_INTERVAL);
}
int main(int argc, char *argv[]) {
int opt;
const char *location = DEFAULT_LOCATION;
long requeue_sec = DEFAULT_REQUEUE_INTERVAL;
while ((opt = getopt(argc, argv, "c:r:")) != -1) {
switch (opt) {
case 'r': {
requeue_sec = strtol(optarg, NULL, 10);
if (errno == EINVAL || errno == ERANGE) {
fprintf(stderr, "invalid value for requeue interval: %s\n", optarg);
usage();
exit(EXIT_FAILURE);
}
break;
}
case 'c': {
location = optarg;
break;
}
default: /* '?' */
usage();
exit(EXIT_FAILURE);
}
}
worker_t worker;
worker_init(&worker, location);
int ret = launch(&worker, requeue_sec);
worker_destroy(&worker);
return ret;
}
rename(2)
orflock(2)
) to claim a task. \$\endgroup\$