packages feed

rdkafka-hs-1.1.0.0: test-integration/cbits/producer_ownership.c

/* Test executable only: observe the actual client used by produceva and
 * pause at the enqueue ownership boundary. Ordinary calls go to librdkafka. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <librdkafka/rdkafka.h>
#include <librdkafka/rdkafka_mock.h>

static pthread_mutex_t producer_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t producer_resume = PTHREAD_COND_INITIALIZER;
static int producer_enabled, producer_mode, producer_entered, producer_released;
static const char *property_names[] = {
  "client.id", "message.timeout.ms", "compression.codec", "acks", "queue.buffering.max.messages", "log_level"
};
#define PROPERTY_COUNT (sizeof property_names / sizeof property_names[0])
static char *property_values[PROPERTY_COUNT];
typedef void (*delivery_callback)(rd_kafka_t *, const rd_kafka_message_t *, void *);
static delivery_callback observed_delivery;
static rd_kafka_t *observed_client;

void rdkafka_hs_producer_client_created(rd_kafka_type_t type, rd_kafka_t *client) {
  pthread_mutex_lock(&producer_lock);
  if (producer_enabled && type == RD_KAFKA_PRODUCER) observed_client = client;
  pthread_mutex_unlock(&producer_lock);
}
void rd_kafka_conf_set_dr_msg_cb(rd_kafka_conf_t *conf, delivery_callback callback) {
  __typeof__(&rd_kafka_conf_set_dr_msg_cb) real = (__typeof__(&rd_kafka_conf_set_dr_msg_cb))dlsym(RTLD_NEXT, "rd_kafka_conf_set_dr_msg_cb");
  if (!real) abort();
  real(conf, callback);
  pthread_mutex_lock(&producer_lock);
  if (producer_enabled) observed_delivery = callback;
  pthread_mutex_unlock(&producer_lock);
}
int rdkafka_hs_test_producer_null_delivery(void) {
  pthread_mutex_lock(&producer_lock);
  rd_kafka_t *client = observed_client; delivery_callback callback = observed_delivery;
  int valid = producer_enabled && producer_mode == 6 && client && callback;
  pthread_mutex_unlock(&producer_lock);
  if (!valid) abort();
  /* A callback entry can change thread-local errno; zero exercises the
   * native generic-failure fallback without assuming errno survives RTS entry. */
  errno = 0;
  callback(client, NULL, NULL);
  return RD_KAFKA_RESP_ERR__FAIL;
}
int rdkafka_hs_test_producer_without_opaque(const char *topic) {
  pthread_mutex_lock(&producer_lock);
  rd_kafka_t *client = observed_client;
  int valid = producer_enabled && producer_mode == 6 && client;
  pthread_mutex_unlock(&producer_lock);
  if (!valid) abort();
  /* Real native enqueue intentionally has no Haskell per-message opaque root. */
  return rd_kafka_producev(client, RD_KAFKA_V_TOPIC(topic), RD_KAFKA_V_PARTITION(0),
      RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY), RD_KAFKA_V_KEY("native-key", 10),
      RD_KAFKA_V_VALUE("native-without-opaque", 21), RD_KAFKA_V_END);
}
int rdkafka_hs_test_producer_header_failure(void) {
  pthread_mutex_lock(&producer_lock);
  int result = producer_enabled && producer_mode == 7;
  pthread_mutex_unlock(&producer_lock);
  return result;
}

static void clear_properties(void) {
  for (size_t i = 0; i < PROPERTY_COUNT; ++i) {
    free(property_values[i]);
    property_values[i] = NULL;
  }
}
void rdkafka_hs_test_producer_begin(int mode) {
  pthread_mutex_lock(&producer_lock);
  clear_properties();
  producer_enabled = 1;
  producer_mode = mode;
  producer_entered = producer_released = 0;
  observed_delivery = NULL; observed_client = NULL;
  pthread_mutex_unlock(&producer_lock);
}
void rdkafka_hs_test_producer_end(void) {
  pthread_mutex_lock(&producer_lock);
  producer_enabled = 0;
  producer_released = 1;
  observed_delivery = NULL; observed_client = NULL;
  clear_properties();
  pthread_cond_broadcast(&producer_resume);
  pthread_mutex_unlock(&producer_lock);
}
void rdkafka_hs_test_producer_release(void) {
  pthread_mutex_lock(&producer_lock);
  producer_released = 1;
  pthread_cond_broadcast(&producer_resume);
  pthread_mutex_unlock(&producer_lock);
}
int rdkafka_hs_test_producer_entered(void) {
  int result;
  pthread_mutex_lock(&producer_lock);
  result = producer_entered;
  pthread_mutex_unlock(&producer_lock);
  return result;
}
const char *rdkafka_hs_test_producer_property(int index) {
  return index >= 0 && (size_t)index < PROPERTY_COUNT ? property_values[index] : NULL;
}

rd_kafka_error_t *rd_kafka_produceva(rd_kafka_t *client, const rd_kafka_vu_t *values, size_t count) {
  __typeof__(&rd_kafka_produceva) real = (__typeof__(&rd_kafka_produceva))dlsym(RTLD_NEXT, "rd_kafka_produceva");
  if (!real) { fprintf(stderr, "producer ownership: %s\n", dlerror()); abort(); }
  pthread_mutex_lock(&producer_lock);
  int enabled = producer_enabled, mode = producer_mode;
  if (enabled) {
    clear_properties();
    const rd_kafka_conf_t *conf = rd_kafka_conf(client);
    rd_kafka_topic_conf_t *topic_conf = rd_kafka_default_topic_conf_dup(client);
    for (size_t i = 0; i < PROPERTY_COUNT; ++i) {
      size_t length = 0;
      int topic_property = rd_kafka_conf_get(conf, property_names[i], NULL, &length) != RD_KAFKA_CONF_OK;
      /* An untouched client need not have an allocated default-topic config.
       * The native duplication API supplies the same native topic defaults. */
      if (topic_property && rd_kafka_topic_conf_get(topic_conf, property_names[i], NULL, &length) != RD_KAFKA_CONF_OK) continue;
      property_values[i] = calloc(length + 1, 1);
      if (!property_values[i]) abort();
      size_t capacity = length;
      rd_kafka_conf_res_t result = topic_property
        ? rd_kafka_topic_conf_get(topic_conf, property_names[i], property_values[i], &capacity)
        : rd_kafka_conf_get(conf, property_names[i], property_values[i], &capacity);
      if (result != RD_KAFKA_CONF_OK || capacity > length) abort();
    }
    rd_kafka_topic_conf_destroy(topic_conf);
  }
  if (enabled && mode == 1) {
    /* The Haskell StablePtr exists; the native client has not accepted it. */
    producer_entered = 1;
    while (!producer_released) pthread_cond_wait(&producer_resume, &producer_lock);
    pthread_mutex_unlock(&producer_lock);
    return rd_kafka_error_new(RD_KAFKA_RESP_ERR__QUEUE_FULL, "%s", "controlled enqueue rejection");
  }
  if (enabled && mode >= 3 && mode <= 5) {
    /* These calls reject before native transfer. Exercise a decoder failure,
     * a contradictory non-null success-code object, and a valid UNKNOWN code
     * with an unrelated errno. No message or delivery report is created. */
    rd_kafka_resp_err_t code = mode == 3 ? (rd_kafka_resp_err_t)32767
      : mode == 4 ? RD_KAFKA_RESP_ERR_NO_ERROR : RD_KAFKA_RESP_ERR_UNKNOWN;
    producer_entered = 1;
    pthread_mutex_unlock(&producer_lock);
    rd_kafka_error_t *error = rd_kafka_error_new(code, "%s", "controlled native error object");
    if (mode == 5) errno = EINVAL;
    return error;
  }
  pthread_mutex_unlock(&producer_lock);
  rd_kafka_error_t *result = real(client, values, count);
  const char *safety_probe = getenv("RDKAFKA_TEST_PRODUCER_SAFETY_PROBE");
  if (!result && safety_probe)
    fprintf(stderr, "producer safety probe: native enqueue accepted (%s)\n", safety_probe);
  if (enabled && mode == 2 && !result) {
    /* Native ownership has passed, but the caller has not seen the result.
     * Another Haskell thread may now serve the real delivery report. */
    pthread_mutex_lock(&producer_lock);
    producer_entered = 1;
    while (!producer_released) pthread_cond_wait(&producer_resume, &producer_lock);
    pthread_mutex_unlock(&producer_lock);
  }
  return result;
}

/* Native protocol fixture: delay only Produce responses and observe actual
 * received Produce requests before testing in-flight purge behavior. This
 * does not replace the real-broker configuration and record round trips. */
struct producer_mock { rd_kafka_t *anchor; rd_kafka_mock_cluster_t *cluster; };
struct producer_mock *rdkafka_hs_test_producer_mock_new(void) {
  char error[512];
  struct producer_mock *mock = calloc(1, sizeof *mock);
  if (!mock) abort();
  rd_kafka_conf_t *conf = rd_kafka_conf_new();
  rd_kafka_conf_set(conf, "log_level", "0", error, sizeof error);
  mock->anchor = rd_kafka_new(RD_KAFKA_PRODUCER, conf, error, sizeof error);
  if (!mock->anchor) { rd_kafka_conf_destroy(conf); free(mock); return NULL; }
  mock->cluster = rd_kafka_mock_cluster_new(mock->anchor, 1);
  if (!mock->cluster) { rd_kafka_destroy(mock->anchor); free(mock); return NULL; }
  if (rd_kafka_mock_topic_create(mock->cluster, "producer-inflight", 1, 1)) abort();
  rd_kafka_mock_start_request_tracking(mock->cluster);
  return mock;
}
void rdkafka_hs_test_producer_mock_destroy(struct producer_mock *mock) {
  rd_kafka_mock_cluster_destroy(mock->cluster);
  rd_kafka_destroy(mock->anchor);
  free(mock);
}
const char *rdkafka_hs_test_producer_mock_brokers(struct producer_mock *mock) {
  return rd_kafka_mock_cluster_bootstraps(mock->cluster);
}
int rdkafka_hs_test_producer_mock_delay(struct producer_mock *mock, int milliseconds) {
  return rd_kafka_mock_broker_push_request_error_rtts(mock->cluster, 1, 0, 1,
                                                     RD_KAFKA_RESP_ERR_NO_ERROR, milliseconds);
}
int rdkafka_hs_test_producer_mock_received(struct producer_mock *mock) {
  size_t count;
  int result = 0;
  rd_kafka_mock_request_t **requests = rd_kafka_mock_get_requests(mock->cluster, &count);
  for (size_t i = 0; i < count; ++i)
    if (rd_kafka_mock_request_api_key(requests[i]) == 0) ++result;
  rd_kafka_mock_request_destroy_array(requests, count);
  return result;
}