commit 804df0bdf801c583e2f935361ab18edfb4f5261a
parent 91cadd58f63b9c00d22932fefec0c0a8d10278e7
Author: William Casarin <jb55@jb55.com>
Date:   Fri,  4 Aug 2023 16:26:21 -0700
queue: add protected queue implementation
Link: https://chat.openai.com/share/e4e17d6b-e664-44c1-b4bd-220ad8cb0df3
Co-authored-by: GPT4
Diffstat:
| A | protected_queue.h | | | 161 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| M | test.c | | | 114 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
2 files changed, 275 insertions(+), 0 deletions(-)
diff --git a/protected_queue.h b/protected_queue.h
@@ -0,0 +1,161 @@
+/*
+ *    This header file provides a thread-safe queue implementation for generic
+ *    data elements. It uses POSIX threads (pthreads) to ensure thread safety.
+ *    The queue allows for pushing and popping elements, with the ability to
+ *    block or non-block on pop operations. Users are responsible for providing
+ *    memory for the queue buffer and ensuring its correct lifespan.
+ *
+ *         Author:  William Casarin
+ */
+
+#ifndef PROT_QUEUE_H
+#define PROT_QUEUE_H
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include "cursor.h"
+
+#define BUFFER_SIZE 100
+
+/* 
+ * The prot_queue structure represents a thread-safe queue that can hold
+ * generic data elements.
+ */
+struct prot_queue {
+	unsigned char *buf;
+	int buflen;
+
+	int head;
+	int tail;
+	int count;
+	int elem_size;
+
+	pthread_mutex_t mutex;
+	pthread_cond_t cond;
+};
+
+
+/* 
+ * Initialize the queue. 
+ * Params:
+ * q         - Pointer to the queue.
+ * buf       - Buffer for holding data elements.
+ * buflen    - Length of the buffer.
+ * elem_size - Size of each data element.
+ * Returns 1 if successful, 0 otherwise.
+ */
+static inline int prot_queue_init(struct prot_queue* q, void* buf, int buflen,
+				  int elem_size)
+{
+	// buffer elements must fit nicely in the buffer
+	if (buflen == 0 || buflen % elem_size != 0)
+		return 0;
+
+	q->head = 0;
+	q->tail = 0;
+	q->count = 0;
+	q->buf = buf;
+	q->buflen = buflen;
+	q->elem_size = elem_size;
+
+	pthread_mutex_init(&q->mutex, NULL);
+	pthread_cond_init(&q->cond, NULL);
+
+	return 1;
+}
+
+/* 
+ * Return the capacity of the queue.
+ * q    - Pointer to the queue.
+ */
+static inline int prot_queue_capacity(struct prot_queue *q) {
+	return q->buflen / q->elem_size;
+}
+
+/* 
+ * Push an element onto the queue.
+ * Params:
+ * q    - Pointer to the queue.
+ * data - Pointer to the data element to be pushed.
+ *
+ * Returns 1 if successful, 0 if the queue is full.
+ */
+static inline int prot_queue_push(struct prot_queue* q, void *data)
+{
+	int cap;
+
+	pthread_mutex_lock(&q->mutex);
+
+	cap = prot_queue_capacity(q);
+	if (q->count == cap) {
+		// only signal if the push was sucessful
+		pthread_mutex_unlock(&q->mutex);
+		return 0;
+	}
+
+	memcpy(&q->buf[q->tail * q->elem_size], data, q->elem_size);
+	q->tail = (q->tail + 1) % cap;
+	q->count++;
+
+	pthread_cond_signal(&q->cond);
+	pthread_mutex_unlock(&q->mutex);
+
+	return 1;
+}
+
+/* 
+ * Try to pop an element from the queue without blocking.
+ * Params:
+ * q    - Pointer to the queue.
+ * data - Pointer to where the popped data will be stored.
+ * Returns 1 if successful, 0 if the queue is empty.
+ */
+static inline int prot_queue_try_pop(struct prot_queue *q, void *data) {
+	pthread_mutex_lock(&q->mutex);
+
+	if (q->count == 0) {
+		pthread_mutex_unlock(&q->mutex);
+		return 0;
+	}
+
+	memcpy(data, &q->buf[q->head * q->elem_size], q->elem_size);
+	q->head = (q->head + 1) % prot_queue_capacity(q);
+	q->count--;
+
+	pthread_mutex_unlock(&q->mutex);
+	return 1;
+}
+
+
+/* 
+ * Pop an element from the queue. Blocks if the queue is empty.
+ * Params:
+ * q    - Pointer to the queue.
+ * data - Pointer to where the popped data will be stored.
+ */
+static inline void prot_queue_pop(struct prot_queue *q, void *data) {
+	pthread_mutex_lock(&q->mutex);
+
+	while (q->count == 0)
+		pthread_cond_wait(&q->cond, &q->mutex);
+
+	memcpy(data, &q->buf[q->head * q->elem_size], q->elem_size);
+	q->head = (q->head + 1) % prot_queue_capacity(q);
+	q->count--;
+
+	pthread_mutex_unlock(&q->mutex);
+}
+
+/* 
+ * Destroy the queue. Releases resources associated with the queue.
+ * Params:
+ * q - Pointer to the queue.
+ */
+static inline void prot_queue_destroy(struct prot_queue* q) {
+	pthread_mutex_destroy(&q->mutex);
+	pthread_cond_destroy(&q->cond);
+}
+
+#endif // PROT_QUEUE_H
diff --git a/test.c b/test.c
@@ -2,6 +2,7 @@
 #include "nostrdb.h"
 #include "hex.h"
 #include "io.h"
+#include "protected_queue.h"
 
 #include <stdio.h>
 #include <assert.h>
@@ -351,6 +352,112 @@ static void test_tce() {
 #undef JSON
 }
 
+#define TEST_BUF_SIZE 10  // For simplicity
+
+static void test_queue_init_pop_push() {
+	struct prot_queue q;
+	int buffer[TEST_BUF_SIZE];
+	int data;
+
+	// Initialize
+	assert(prot_queue_init(&q, buffer, sizeof(buffer), sizeof(int)) == 1);
+
+	// Push and Pop
+	data = 5;
+	assert(prot_queue_push(&q, &data) == 1);
+	prot_queue_pop(&q, &data);
+	assert(data == 5);
+
+	// Push to full, and then fail to push
+	for (int i = 0; i < TEST_BUF_SIZE; i++) {
+		assert(prot_queue_push(&q, &i) == 1);
+	}
+	assert(prot_queue_push(&q, &data) == 0);  // Should fail as queue is full
+
+	// Pop to empty, and then fail to pop
+	for (int i = 0; i < TEST_BUF_SIZE; i++) {
+		assert(prot_queue_try_pop(&q, &data) == 1);
+		assert(data == i);
+	}
+	assert(prot_queue_try_pop(&q, &data) == 0);  // Should fail as queue is empty
+}
+
+// This function will be used by threads to test thread safety.
+void* thread_func(void* arg) {
+	struct prot_queue* q = (struct prot_queue*) arg;
+	int data;
+
+	for (int i = 0; i < 100; i++) {
+		data = i;
+		prot_queue_push(q, &data);
+		prot_queue_pop(q, &data);
+	}
+	return NULL;
+}
+
+static void test_queue_thread_safety() {
+	struct prot_queue q;
+	int buffer[TEST_BUF_SIZE];
+	pthread_t threads[2];
+
+	assert(prot_queue_init(&q, buffer, sizeof(buffer), sizeof(int)) == 1);
+
+	// Create threads
+	for (int i = 0; i < 2; i++) {
+		pthread_create(&threads[i], NULL, thread_func, &q);
+	}
+
+	// Join threads
+	for (int i = 0; i < 2; i++) {
+		pthread_join(threads[i], NULL);
+	}
+
+	// After all operations, the queue should be empty
+	int data;
+	assert(prot_queue_try_pop(&q, &data) == 0);
+}
+
+static void test_queue_boundary_conditions() {
+    struct prot_queue q;
+    int buffer[TEST_BUF_SIZE];
+    int data;
+
+    // Initialize
+    assert(prot_queue_init(&q, buffer, sizeof(buffer), sizeof(int)) == 1);
+
+    // Push to full
+    for (int i = 0; i < TEST_BUF_SIZE; i++) {
+        assert(prot_queue_push(&q, &i) == 1);
+    }
+
+    // Try to push to a full queue
+    int old_head = q.head;
+    int old_tail = q.tail;
+    int old_count = q.count;
+    assert(prot_queue_push(&q, &data) == 0);
+    
+    // Assert the queue's state has not changed
+    assert(old_head == q.head);
+    assert(old_tail == q.tail);
+    assert(old_count == q.count);
+
+    // Pop to empty
+    for (int i = 0; i < TEST_BUF_SIZE; i++) {
+        assert(prot_queue_try_pop(&q, &data) == 1);
+    }
+
+    // Try to pop from an empty queue
+    old_head = q.head;
+    old_tail = q.tail;
+    old_count = q.count;
+    assert(prot_queue_try_pop(&q, &data) == 0);
+    
+    // Assert the queue's state has not changed
+    assert(old_head == q.head);
+    assert(old_tail == q.tail);
+    assert(old_count == q.count);
+}
+
 int main(int argc, const char *argv[]) {
 	test_basic_event();
 	test_empty_tags();
@@ -362,4 +469,11 @@ int main(int argc, const char *argv[]) {
 	test_tce_eose();
 	test_tce_command_result_empty_msg();
 	test_content_len();
+
+	// protected queue tests
+	test_queue_init_pop_push();
+	test_queue_thread_safety();
+	test_queue_boundary_conditions();
+
+	printf("All tests passed!\n");       // Print this if all tests pass.
 }