hermann 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1031 @@
1
+ /*
2
+ * hermann_lib.c - Ruby wrapper for the librdkafka library
3
+ *
4
+ * Copyright (c) 2014 Stan Campbell
5
+ * Copyright (c) 2014 Lookout, Inc.
6
+ * Copyright (c) 2014 R. Tyler Croy
7
+ *
8
+ * All rights reserved.
9
+ *
10
+ * Redistribution and use in source and binary forms, with or without
11
+ * modification, are permitted provided that the following conditions are met:
12
+ *
13
+ * 1. Redistributions of source code must retain the above copyright notice,
14
+ * this list of conditions and the following disclaimer.
15
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
16
+ * this list of conditions and the following disclaimer in the documentation
17
+ * and/or other materials provided with the distribution.
18
+ *
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ * POSSIBILITY OF SUCH DAMAGE.
30
+ */
31
+
32
+ /* Much of the librdkafka library calls were lifted from rdkafka_example.c */
33
+
34
+ #include "hermann_lib.h"
35
+
36
+ /**
37
+ * Convenience function
38
+ *
39
+ * @param config HermannInstanceConfig
40
+ * @param outputStream FILE*
41
+ *
42
+ * Log the contents of the configuration to the provided stream.
43
+ */
44
+ void fprintf_hermann_instance_config(HermannInstanceConfig *config,
45
+ FILE *outputStream) {
46
+
47
+ const char *topic = NULL;
48
+ const char *brokers = NULL;
49
+ int isRkSet = -1;
50
+ int isRktSet = -1;
51
+ int partition = -1;
52
+ int isInitialized = -1;
53
+
54
+ if (NULL == config) {
55
+ fprintf(outputStream, "NULL configuration");
56
+ }
57
+ else {
58
+ isRkSet = (config->rk != NULL);
59
+ isRktSet = (config->rkt != NULL);
60
+
61
+ if (NULL == config->topic) {
62
+ topic = NULL;
63
+ }
64
+ else {
65
+ topic = config->topic;
66
+ }
67
+
68
+ if (NULL == config->brokers) {
69
+ brokers = "NULL";
70
+ }
71
+ else {
72
+ brokers = config->brokers;
73
+ }
74
+
75
+ partition = config->partition;
76
+ isInitialized = config->isInitialized;
77
+ }
78
+
79
+ fprintf(outputStream, "{ topic: %s, brokers: %s, partition: %d, isInitialized: %d, rkSet: %d, rkTSet: %d }\n",
80
+ topic, brokers, partition, isInitialized, isRkSet, isRktSet );
81
+ }
82
+
83
+ /**
84
+ * Message delivery report callback.
85
+ * Called once for each message.
86
+ *
87
+ */
88
+ static void msg_delivered(rd_kafka_t *rk,
89
+ const rd_kafka_message_t *message,
90
+ void *ctx) {
91
+ hermann_push_ctx_t *push_ctx;
92
+ VALUE is_error = Qfalse;
93
+ ID hermann_result_fulfill_method = rb_intern("internal_set_value");
94
+
95
+ TRACER("ctx: %p, err: %i\n", ctx, message->err);
96
+
97
+ if (message->err) {
98
+ is_error = Qtrue;
99
+ fprintf(stderr, "%% Message delivery failed: %s\n",
100
+ rd_kafka_err2str(message->err));
101
+ /* todo: should raise an error? */
102
+ }
103
+
104
+ /* according to @edenhill rd_kafka_message_t._private is ABI safe to call
105
+ * and represents the `msg_opaque` argument passed into `rd_kafka_produce`
106
+ */
107
+ if (NULL != message->_private) {
108
+ push_ctx = (hermann_push_ctx_t *)message->_private;
109
+
110
+ if (!message->err) {
111
+ /* if we have not errored, great! let's say we're connected */
112
+ push_ctx->producer->isConnected = 1;
113
+ }
114
+
115
+ /* call back into our Hermann::Result if it exists, discarding the
116
+ * return value
117
+ */
118
+ if (NULL != push_ctx->result) {
119
+ rb_funcall(push_ctx->result,
120
+ hermann_result_fulfill_method,
121
+ 2,
122
+ rb_str_new((char *)message->payload, message->len), /* value */
123
+ is_error /* is_error */ );
124
+ }
125
+ free(push_ctx);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Producer partitioner callback.
131
+ * Used to determine the target partition within a topic for production.
132
+ *
133
+ * Returns an integer partition number or RD_KAFKA_PARTITION_UA if no
134
+ * available partition could be determined.
135
+ *
136
+ * @param rkt rd_kafka_topic_t* the topic
137
+ * @param keydata void* key information for calculating the partition
138
+ * @param keylen size_t key size
139
+ * @param partition_cnt int32_t the count of the number of partitions
140
+ * @param rkt_opaque void* opaque topic info
141
+ * @param msg_opaque void* opaque message info
142
+ */
143
+ static int32_t producer_partitioner_callback(const rd_kafka_topic_t *rkt,
144
+ const void *keydata,
145
+ size_t keylen,
146
+ int32_t partition_cnt,
147
+ void *rkt_opaque,
148
+ void *msg_opaque) {
149
+ /* Pick a random partition */
150
+ int retry = 0;
151
+ for (; retry < partition_cnt; retry++) {
152
+ int32_t partition = rand() % partition_cnt;
153
+ if (rd_kafka_topic_partition_available(rkt, partition)) {
154
+ break; /* this one will do */
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * hexdump
161
+ *
162
+ * Write the given payload to file in hex notation.
163
+ *
164
+ * @param fp FILE* the file into which to write
165
+ * @param name char* name
166
+ * @param ptr void* payload
167
+ * @param len size_t payload length
168
+ */
169
+ static void hexdump(FILE *fp,
170
+ const char *name,
171
+ const void *ptr,
172
+ size_t len) {
173
+ const char *p = (const char *)ptr;
174
+ unsigned int of = 0;
175
+
176
+
177
+ if (name) {
178
+ fprintf(fp, "%s hexdump (%zd bytes):\n", name, len);
179
+ }
180
+
181
+ for (of = 0 ; of < len ; of += 16) {
182
+ char hexen[16*3+1];
183
+ char charen[16+1];
184
+ unsigned int hof = 0;
185
+
186
+ unsigned int cof = 0;
187
+ unsigned int i;
188
+
189
+ for (i = of ; i < of + 16 && i < len ; i++) {
190
+ hof += sprintf(hexen+hof, "%02x ", p[i] & 0xff);
191
+ cof += sprintf(charen+cof, "%c",
192
+ isprint((int)p[i]) ? p[i] : '.');
193
+ }
194
+ fprintf(fp, "%08x: %-48s %-16s\n",
195
+ of, hexen, charen);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * msg_consume
201
+ *
202
+ * Callback on message receipt.
203
+ *
204
+ * @param rkmessage rd_kafka_message_t* the message
205
+ * @param opaque void* opaque context
206
+ */
207
+ static void msg_consume(rd_kafka_message_t *rkmessage,
208
+ void *opaque) {
209
+
210
+ HermannInstanceConfig* cfg;
211
+
212
+ cfg = (HermannInstanceConfig*)opaque;
213
+
214
+ if (rkmessage->err) {
215
+ if (rkmessage->err == RD_KAFKA_RESP_ERR__PARTITION_EOF) {
216
+ fprintf(stderr,
217
+ "%% Consumer reached end of %s [%"PRId32"] "
218
+ "message queue at offset %"PRId64"\n",
219
+ rd_kafka_topic_name(rkmessage->rkt),
220
+ rkmessage->partition, rkmessage->offset);
221
+
222
+ if (cfg->exit_eof) {
223
+ cfg->run = 0;
224
+ }
225
+
226
+ return;
227
+ }
228
+
229
+ fprintf(stderr, "%% Consume error for topic \"%s\" [%"PRId32"] "
230
+ "offset %"PRId64": %s\n",
231
+ rd_kafka_topic_name(rkmessage->rkt),
232
+ rkmessage->partition,
233
+ rkmessage->offset,
234
+ rd_kafka_message_errstr(rkmessage));
235
+ return;
236
+ }
237
+
238
+ if (DEBUG && rkmessage->key_len) {
239
+ if (output == OUTPUT_HEXDUMP) {
240
+ hexdump(stdout, "Message Key",
241
+ rkmessage->key, rkmessage->key_len);
242
+ }
243
+ else {
244
+ printf("Key: %.*s\n",
245
+ (int)rkmessage->key_len, (char *)rkmessage->key);
246
+ }
247
+ }
248
+
249
+ if (output == OUTPUT_HEXDUMP) {
250
+ if (DEBUG) {
251
+ hexdump(stdout, "Message Payload", rkmessage->payload, rkmessage->len);
252
+ }
253
+ }
254
+ else {
255
+ if (DEBUG) {
256
+ printf("%.*s\n", (int)rkmessage->len, (char *)rkmessage->payload);
257
+ }
258
+ }
259
+
260
+ // Yield the data to the Consumer's block
261
+ if (rb_block_given_p()) {
262
+ VALUE value = rb_str_new((char *)rkmessage->payload, rkmessage->len);
263
+ rb_yield(value);
264
+ }
265
+ else {
266
+ if (DEBUG) {
267
+ fprintf(stderr, "No block given\n"); // todo: should this be an error?
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * logger
274
+ *
275
+ * Kafka logger callback (optional)
276
+ *
277
+ * todo: introduce better logging
278
+ *
279
+ * @param rk rd_kafka_t the producer or consumer
280
+ * @param level int the log level
281
+ * @param fac char* something of which I am unaware
282
+ * @param buf char* the log message
283
+ */
284
+ static void logger(const rd_kafka_t *rk,
285
+ int level,
286
+ const char *fac,
287
+ const char *buf) {
288
+ struct timeval tv;
289
+ gettimeofday(&tv, NULL);
290
+ fprintf(stderr, "%u.%03u RDKAFKA-%i-%s: %s: %s\n",
291
+ (int)tv.tv_sec, (int)(tv.tv_usec / 1000),
292
+ level, fac, rd_kafka_name(rk), buf);
293
+ }
294
+
295
+ /**
296
+ * consumer_init_kafka
297
+ *
298
+ * Initialize the Kafka context and instantiate a consumer.
299
+ *
300
+ * @param config HermannInstanceConfig* pointer to the instance configuration for this producer or consumer
301
+ */
302
+ void consumer_init_kafka(HermannInstanceConfig* config) {
303
+
304
+ TRACER("configuring rd_kafka\n");
305
+
306
+ config->quiet = !isatty(STDIN_FILENO);
307
+
308
+ /* Kafka configuration */
309
+ config->conf = rd_kafka_conf_new();
310
+
311
+ /* Topic configuration */
312
+ config->topic_conf = rd_kafka_topic_conf_new();
313
+
314
+ /* Create Kafka handle */
315
+ if (!(config->rk = rd_kafka_new(RD_KAFKA_CONSUMER, config->conf,
316
+ config->errstr, sizeof(config->errstr)))) {
317
+ fprintf(stderr, "%% Failed to create new consumer: %s\n", config->errstr);
318
+ rb_raise(rb_eRuntimeError, "%% Failed to create new consumer: %s\n", config->errstr);
319
+ }
320
+
321
+ /* Set logger */
322
+ rd_kafka_set_logger(config->rk, logger);
323
+ rd_kafka_set_log_level(config->rk, LOG_DEBUG);
324
+
325
+ /* TODO: offset calculation */
326
+ config->start_offset = RD_KAFKA_OFFSET_END;
327
+
328
+ /* Add brokers */
329
+ if (rd_kafka_brokers_add(config->rk, config->brokers) == 0) {
330
+ fprintf(stderr, "%% No valid brokers specified\n");
331
+ rb_raise(rb_eRuntimeError, "No valid brokers specified");
332
+ return;
333
+ }
334
+
335
+ /* Create topic */
336
+ config->rkt = rd_kafka_topic_new(config->rk, config->topic, config->topic_conf);
337
+
338
+ /* We're now initialized */
339
+ config->isInitialized = 1;
340
+ }
341
+
342
+ // Ruby gem extensions
343
+
344
+ #ifdef RB_THREAD_BLOCKING_REGION
345
+ /* NOTE: We only need this method defined if RB_THREAD_BLOCKING_REGION is
346
+ * defined, otherwise it's unused
347
+ */
348
+
349
+ /**
350
+ * Callback invoked if Ruby needs to stop our Consumer's IO loop for any reason
351
+ * (system exit, etc.)
352
+ */
353
+ static void consumer_consume_stop_callback(void *ptr) {
354
+ HermannInstanceConfig* config = (HermannInstanceConfig*)ptr;
355
+
356
+ TRACER("stopping callback (%p)\n", ptr);
357
+
358
+ config->run = 0;
359
+ }
360
+ #endif
361
+
362
+ /**
363
+ * Loop on a timeout to receive messages from Kafka. When the consumer_consume_stop_callback is invoked by Ruby,
364
+ * we'll break out of our loop and return.
365
+ */
366
+ void consumer_consume_loop(HermannInstanceConfig* consumerConfig) {
367
+
368
+ TRACER("\n");
369
+
370
+ while (consumerConfig->run) {
371
+ if (rd_kafka_consume_callback(consumerConfig->rkt, consumerConfig->partition,
372
+ 1000/*timeout*/,
373
+ msg_consume,
374
+ consumerConfig) < 0) {
375
+ fprintf(stderr, "%% Error: %s\n", rd_kafka_err2str( rd_kafka_errno2err(errno)));
376
+ }
377
+
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Hermann::Consumer.consume
383
+ *
384
+ * Begin listening on the configured topic for messages. msg_consume will be called on each message received.
385
+ *
386
+ * @param VALUE self the Ruby object for this consumer
387
+ */
388
+ static VALUE consumer_consume(VALUE self) {
389
+
390
+ HermannInstanceConfig* consumerConfig;
391
+
392
+ TRACER("starting consume\n");
393
+
394
+ Data_Get_Struct(self, HermannInstanceConfig, consumerConfig);
395
+
396
+ if ((NULL == consumerConfig->topic) ||
397
+ (0 == strlen(consumerConfig->topic))) {
398
+ fprintf(stderr, "Topic is null!\n");
399
+ rb_raise(rb_eRuntimeError, "Topic cannot be empty");
400
+ return self;
401
+ }
402
+
403
+ if (!consumerConfig->isInitialized) {
404
+ consumer_init_kafka(consumerConfig);
405
+ }
406
+
407
+ /* Start consuming */
408
+ if (rd_kafka_consume_start(consumerConfig->rkt, consumerConfig->partition, consumerConfig->start_offset) == -1) {
409
+ fprintf(stderr, "%% Failed to start consuming: %s\n",
410
+ rd_kafka_err2str(rd_kafka_errno2err(errno)));
411
+ rb_raise(rb_eRuntimeError,
412
+ rd_kafka_err2str(rd_kafka_errno2err(errno)));
413
+ return Qnil;
414
+ }
415
+
416
+ #ifdef RB_THREAD_BLOCKING_REGION
417
+ /** The consumer will listen for incoming messages in a loop, timing out and checking the consumerConfig->run
418
+ * flag every second.
419
+ *
420
+ * Call rb_thread_blocking_region to release the GVM lock and allow Ruby to amuse itself while we wait on
421
+ * IO from Kafka.
422
+ *
423
+ * If Ruby needs to interrupt the consumer loop, the stop callback will be invoked and the loop should exit.
424
+ */
425
+ rb_thread_blocking_region(consumer_consume_loop,
426
+ consumerConfig,
427
+ consumer_consume_stop_callback,
428
+ consumerConfig);
429
+ #else
430
+ consumer_consume_loop(consumerConfig);
431
+ #endif
432
+
433
+
434
+ /* Stop consuming */
435
+ rd_kafka_consume_stop(consumerConfig->rkt, consumerConfig->partition);
436
+
437
+ return Qnil;
438
+ }
439
+
440
+
441
+ static void producer_error_callback(rd_kafka_t *rk,
442
+ int error,
443
+ const char *reason,
444
+ void *opaque) {
445
+ hermann_conf_t *conf = (hermann_conf_t *)rd_kafka_opaque(rk);
446
+
447
+ TRACER("error (%i): %s\n", error, reason);
448
+
449
+ conf->isErrored = error;
450
+
451
+ if (error) {
452
+ /* If we have an old error string in here we need to make sure to
453
+ * free() it before we allocate a new string
454
+ */
455
+ if (NULL != conf->error) {
456
+ free(conf->error);
457
+ }
458
+
459
+ /* Grab the length of the string plus the null character */
460
+ size_t error_length = strnlen(reason, HERMANN_MAX_ERRSTR_LEN) + 1;
461
+ conf->error = (char *)malloc((sizeof(char) * error_length));
462
+ (void)strncpy(conf->error, reason, error_length);
463
+ }
464
+ }
465
+
466
+
467
+ /**
468
+ * producer_init_kafka
469
+ *
470
+ * Initialize the producer instance, setting up the Kafka topic and context.
471
+ *
472
+ * @param self VALUE Instance of the Producer Ruby object
473
+ * @param config HermannInstanceConfig* the instance configuration associated with this producer.
474
+ */
475
+ void producer_init_kafka(VALUE self, HermannInstanceConfig* config) {
476
+
477
+ TRACER("initing (%p)\n", config);
478
+
479
+ config->quiet = !isatty(STDIN_FILENO);
480
+
481
+ /* Kafka configuration */
482
+ config->conf = rd_kafka_conf_new();
483
+
484
+
485
+ /* Add our `self` to the opaque pointer for error and logging callbacks
486
+ */
487
+ rd_kafka_conf_set_opaque(config->conf, (void*)config);
488
+ rd_kafka_conf_set_error_cb(config->conf, producer_error_callback);
489
+
490
+ /* Topic configuration */
491
+ config->topic_conf = rd_kafka_topic_conf_new();
492
+
493
+ /* Set up a message delivery report callback.
494
+ * It will be called once for each message, either on successful
495
+ * delivery to broker, or upon failure to deliver to broker. */
496
+ rd_kafka_conf_set_dr_msg_cb(config->conf, msg_delivered);
497
+
498
+ /* Create Kafka handle */
499
+ if (!(config->rk = rd_kafka_new(RD_KAFKA_PRODUCER,
500
+ config->conf,
501
+ config->errstr,
502
+ sizeof(config->errstr)))) {
503
+ /* TODO: Use proper logger */
504
+ fprintf(stderr,
505
+ "%% Failed to create new producer: %s\n", config->errstr);
506
+ rb_raise(rb_eRuntimeError, "%% Failed to create new producer: %s\n", config->errstr);
507
+ }
508
+
509
+ /* Set logger */
510
+ rd_kafka_set_logger(config->rk, logger);
511
+ rd_kafka_set_log_level(config->rk, LOG_DEBUG);
512
+
513
+ if (rd_kafka_brokers_add(config->rk, config->brokers) == 0) {
514
+ /* TODO: Use proper logger */
515
+ fprintf(stderr, "%% No valid brokers specified\n");
516
+ rb_raise(rb_eRuntimeError, "No valid brokers specified");
517
+ return;
518
+ }
519
+
520
+ /* Create topic */
521
+ config->rkt = rd_kafka_topic_new(config->rk, config->topic, config->topic_conf);
522
+
523
+ /* Set the partitioner callback */
524
+ rd_kafka_topic_conf_set_partitioner_cb( config->topic_conf, producer_partitioner_callback);
525
+
526
+ /* We're now initialized */
527
+ config->isInitialized = 1;
528
+
529
+ TRACER("completed kafka init\n");
530
+ }
531
+
532
+ /**
533
+ * producer_push_single
534
+ *
535
+ * @param self VALUE the Ruby producer instance
536
+ * @param message VALUE the ruby String containing the outgoing message.
537
+ * @param result VALUE the Hermann::Result object to be fulfilled when the
538
+ * push completes
539
+ */
540
+ static VALUE producer_push_single(VALUE self, VALUE message, VALUE result) {
541
+
542
+ HermannInstanceConfig* producerConfig;
543
+ /* Context pointer, pointing to `result`, for the librdkafka delivery
544
+ * callback
545
+ */
546
+ hermann_push_ctx_t *delivery_ctx = (hermann_push_ctx_t *)malloc(sizeof(hermann_push_ctx_t));
547
+
548
+ TRACER("self: %p, message: %p, result: %p)\n", self, message, result);
549
+
550
+ Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
551
+
552
+ delivery_ctx->producer = producerConfig;
553
+ delivery_ctx->result = NULL;
554
+
555
+ TRACER("producerConfig: %p\n", producerConfig);
556
+
557
+ if ((NULL == producerConfig->topic) ||
558
+ (0 == strlen(producerConfig->topic))) {
559
+ fprintf(stderr, "Topic is null!\n");
560
+ rb_raise(rb_eRuntimeError, "Topic cannot be empty");
561
+ return self;
562
+ }
563
+
564
+ if (!producerConfig->isInitialized) {
565
+ producer_init_kafka(self, producerConfig);
566
+ }
567
+
568
+ TRACER("kafka initialized\n");
569
+
570
+ /* Only pass result through if it's non-nil */
571
+ if (Qnil != result) {
572
+ delivery_ctx->result = result;
573
+ TRACER("setting result: %p\n", result);
574
+ }
575
+
576
+ TRACER("rd_kafka_produce() message of %i bytes\n", RSTRING_LEN(message));
577
+
578
+ /* Send/Produce message. */
579
+ if (-1 == rd_kafka_produce(producerConfig->rkt,
580
+ producerConfig->partition,
581
+ RD_KAFKA_MSG_F_COPY,
582
+ RSTRING_PTR(message),
583
+ RSTRING_LEN(message),
584
+ NULL,
585
+ 0,
586
+ delivery_ctx)) {
587
+ fprintf(stderr, "%% Failed to produce to topic %s partition %i: %s\n",
588
+ rd_kafka_topic_name(producerConfig->rkt), producerConfig->partition,
589
+ rd_kafka_err2str(rd_kafka_errno2err(errno)));
590
+ /* TODO: raise a Ruby exception here, requires a test though */
591
+ }
592
+
593
+ TRACER("returning\n");
594
+
595
+ return self;
596
+ }
597
+
598
+ /**
599
+ * producer_tick
600
+ *
601
+ * This function is responsible for ticking the librdkafka reactor so we can
602
+ * get feedback from the librdkafka threads back into the Ruby environment
603
+ *
604
+ * @param self VALUE the Ruby producer instance
605
+ * @param message VALUE A Ruby FixNum of how many ms we should wait on librdkafka
606
+ */
607
+ static VALUE producer_tick(VALUE self, VALUE timeout) {
608
+ hermann_conf_t *conf = NULL;
609
+ long timeout_ms = 0;
610
+ int events = 0;
611
+
612
+ if (Qnil != timeout) {
613
+ timeout_ms = rb_num2int(timeout);
614
+ }
615
+ else {
616
+ rb_raise(rb_eArgError, "Cannot call `tick` with a nil timeout!\n");
617
+ }
618
+
619
+ Data_Get_Struct(self, hermann_conf_t, conf);
620
+
621
+ /*
622
+ * if the producerConfig is not initialized then we never properly called
623
+ * producer_push_single, so why are we ticking?
624
+ */
625
+ if (!conf->isInitialized) {
626
+ rb_raise(rb_eRuntimeError, "Cannot call `tick` without having ever sent a message\n");
627
+ }
628
+
629
+ events = rd_kafka_poll(conf->rk, timeout_ms);
630
+
631
+ if (conf->isErrored) {
632
+ rb_raise(rb_eStandardError, conf->error);
633
+ }
634
+
635
+ return rb_int_new(events);
636
+ }
637
+
638
+
639
+ static VALUE producer_connect(VALUE self, VALUE timeout) {
640
+ HermannInstanceConfig *producerConfig;
641
+ rd_kafka_resp_err_t err;
642
+ VALUE result = Qfalse;
643
+ int timeout_ms = rb_num2int(timeout);
644
+ struct rd_kafka_metadata *data = NULL;
645
+
646
+ Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
647
+
648
+ if (!producerConfig->isInitialized) {
649
+ producer_init_kafka(self, producerConfig);
650
+ }
651
+
652
+ err = rd_kafka_metadata(producerConfig->rk,
653
+ 0,
654
+ producerConfig->rkt,
655
+ &data,
656
+ timeout_ms);
657
+ TRACER("err: %s (%i)\n", rd_kafka_err2str(err), err);
658
+
659
+ if (RD_KAFKA_RESP_ERR_NO_ERROR == err) {
660
+ TRACER("brokers: %i, topics: %i\n",
661
+ data->broker_cnt,
662
+ data->topic_cnt);
663
+ producerConfig->isConnected = 1;
664
+ result = Qtrue;
665
+ }
666
+ else {
667
+ producerConfig->isErrored = err;
668
+ }
669
+
670
+ rd_kafka_metadata_destroy(data);
671
+
672
+ return result;
673
+ }
674
+
675
+ static VALUE producer_is_connected(VALUE self) {
676
+ HermannInstanceConfig *producerConfig;
677
+
678
+ Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
679
+
680
+ if (!producerConfig->isInitialized) {
681
+ return Qfalse;
682
+ }
683
+
684
+ if (!producerConfig->isConnected) {
685
+ return Qfalse;
686
+ }
687
+
688
+ return Qtrue;
689
+ }
690
+
691
+ static VALUE producer_is_errored(VALUE self) {
692
+ HermannInstanceConfig *producerConfig;
693
+
694
+ Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
695
+
696
+ if (producerConfig->isErrored) {
697
+ return Qtrue;
698
+ }
699
+
700
+ return Qfalse;
701
+ }
702
+
703
+
704
+ /**
705
+ * consumer_free
706
+ *
707
+ * Callback called when Ruby needs to GC the configuration associated with an Hermann instance.
708
+ *
709
+ * @param p void* the instance of an HermannInstanceConfig to be freed from allocated memory.
710
+ */
711
+ static void consumer_free(void *p) {
712
+
713
+ HermannInstanceConfig* config = (HermannInstanceConfig *)p;
714
+
715
+ #ifdef TRACE
716
+ fprintf(stderr, "consumer_free\n");
717
+ #endif
718
+
719
+ // the p *should* contain a pointer to the consumerConfig which also must be freed
720
+ if (config->rkt != NULL) {
721
+ rd_kafka_topic_destroy(config->rkt);
722
+ }
723
+
724
+ if (config->rk != NULL) {
725
+ rd_kafka_destroy(config->rk);
726
+ }
727
+
728
+ // clean up the struct
729
+ free(config);
730
+ }
731
+
732
+ /**
733
+ * consumer_allocate
734
+ *
735
+ * Allocate and wrap an HermannInstanceConfig for this Consumer object.
736
+ *
737
+ * @param klass VALUE the class of the enclosing Ruby object.
738
+ */
739
+ static VALUE consumer_allocate(VALUE klass) {
740
+
741
+ VALUE obj;
742
+ HermannInstanceConfig* consumerConfig;
743
+
744
+ #ifdef TRACE
745
+ fprintf(stderr, "consumer_free\n");
746
+ #endif
747
+
748
+ consumerConfig = ALLOC(HermannInstanceConfig);
749
+
750
+ // Make sure it's initialized
751
+ consumerConfig->topic = NULL;
752
+ consumerConfig->rk = NULL;
753
+ consumerConfig->rkt = NULL;
754
+ consumerConfig->brokers = NULL;
755
+ consumerConfig->partition = -1;
756
+ consumerConfig->topic_conf = NULL;
757
+ consumerConfig->errstr[0] = 0;
758
+ consumerConfig->conf = NULL;
759
+ consumerConfig->debug = NULL;
760
+ consumerConfig->start_offset = -1;
761
+ consumerConfig->do_conf_dump = -1;
762
+ consumerConfig->run = 0;
763
+ consumerConfig->exit_eof = 0;
764
+ consumerConfig->quiet = 0;
765
+ consumerConfig->isInitialized = 0;
766
+
767
+ obj = Data_Wrap_Struct(klass, 0, consumer_free, consumerConfig);
768
+
769
+ return obj;
770
+ }
771
+
772
+ /**
773
+ * consumer_initialize
774
+ *
775
+ * todo: configure the brokers through passed parameter, later through zk
776
+ *
777
+ * Set up the Consumer's HermannInstanceConfig context.
778
+ *
779
+ * @param self VALUE the Ruby instance of the Consumer
780
+ * @param topic VALUE a Ruby string
781
+ * @param brokers VALUE a Ruby string containing list of host:port
782
+ * @param partition VALUE a Ruby number
783
+ */
784
+ static VALUE consumer_initialize(VALUE self,
785
+ VALUE topic,
786
+ VALUE brokers,
787
+ VALUE partition) {
788
+
789
+ HermannInstanceConfig* consumerConfig;
790
+ char* topicPtr;
791
+ char* brokersPtr;
792
+ int partitionNo;
793
+
794
+ TRACER("initing consumer ruby object\n");
795
+
796
+ topicPtr = StringValuePtr(topic);
797
+ brokersPtr = StringValuePtr(brokers);
798
+ partitionNo = FIX2INT(partition);
799
+ Data_Get_Struct(self, HermannInstanceConfig, consumerConfig);
800
+
801
+ consumerConfig->topic = topicPtr;
802
+ consumerConfig->brokers = brokersPtr;
803
+ consumerConfig->partition = partitionNo;
804
+ consumerConfig->run = 1;
805
+ consumerConfig->exit_eof = 0;
806
+ consumerConfig->quiet = 0;
807
+
808
+ return self;
809
+ }
810
+
811
+ /**
812
+ * consumer_init_copy
813
+ *
814
+ * When copying into a new instance of a Consumer, reproduce the configuration info.
815
+ *
816
+ * @param copy VALUE the Ruby Consumer instance (with configuration) as destination
817
+ * @param orig VALUE the Ruby Consumer instance (with configuration) as source
818
+ *
819
+ */
820
+ static VALUE consumer_init_copy(VALUE copy,
821
+ VALUE orig) {
822
+ HermannInstanceConfig* orig_config;
823
+ HermannInstanceConfig* copy_config;
824
+
825
+ if (copy == orig) {
826
+ return copy;
827
+ }
828
+
829
+ if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)consumer_free) {
830
+ rb_raise(rb_eTypeError, "wrong argument type");
831
+ }
832
+
833
+ Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
834
+ Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
835
+
836
+ // Copy over the data from one struct to the other
837
+ MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
838
+
839
+ return copy;
840
+ }
841
+
842
+ /**
843
+ * producer_free
844
+ *
845
+ * Reclaim memory allocated to the Producer's configuration
846
+ *
847
+ * @param p void* the instance's configuration struct
848
+ */
849
+ static void producer_free(void *p) {
850
+
851
+ HermannInstanceConfig* config = (HermannInstanceConfig *)p;
852
+
853
+ TRACER("dealloc producer ruby object (%p)\n", p);
854
+
855
+
856
+ if (NULL == p) {
857
+ return;
858
+ }
859
+
860
+ // Clean up the topic
861
+ if (NULL != config->rkt) {
862
+ rd_kafka_topic_destroy(config->rkt);
863
+ }
864
+
865
+ // Take care of the producer instance
866
+ if (NULL != config->rk) {
867
+ rd_kafka_destroy(config->rk);
868
+ }
869
+
870
+ // Free the struct
871
+ free(config);
872
+ }
873
+
874
+ /**
875
+ * producer_allocate
876
+ *
877
+ * Allocate the memory for a Producer's configuration
878
+ *
879
+ * @param klass VALUE the class of the Producer
880
+ */
881
+ static VALUE producer_allocate(VALUE klass) {
882
+
883
+ VALUE obj;
884
+ HermannInstanceConfig* producerConfig = ALLOC(HermannInstanceConfig);
885
+
886
+ producerConfig->topic = NULL;
887
+ producerConfig->rk = NULL;
888
+ producerConfig->rkt = NULL;
889
+ producerConfig->brokers = NULL;
890
+ producerConfig->partition = -1;
891
+ producerConfig->topic_conf = NULL;
892
+ producerConfig->errstr[0] = 0;
893
+ producerConfig->conf = NULL;
894
+ producerConfig->debug = NULL;
895
+ producerConfig->start_offset = -1;
896
+ producerConfig->do_conf_dump = -1;
897
+ producerConfig->run = 0;
898
+ producerConfig->exit_eof = 0;
899
+ producerConfig->quiet = 0;
900
+ producerConfig->isInitialized = 0;
901
+ producerConfig->isConnected = 0;
902
+ producerConfig->isErrored = 0;
903
+ producerConfig->error = NULL;
904
+
905
+ obj = Data_Wrap_Struct(klass, 0, producer_free, producerConfig);
906
+
907
+ return obj;
908
+ }
909
+
910
+ /**
911
+ * producer_initialize
912
+ *
913
+ * Set up the configuration context for the Producer instance
914
+ *
915
+ * @param self VALUE the Producer instance
916
+ * @param topic VALUE the Ruby string naming the topic
917
+ * @param brokers VALUE a Ruby string containing host:port pairs separated by commas
918
+ */
919
+ static VALUE producer_initialize(VALUE self,
920
+ VALUE topic,
921
+ VALUE brokers) {
922
+
923
+ HermannInstanceConfig* producerConfig;
924
+ char* topicPtr;
925
+ char* brokersPtr;
926
+
927
+ TRACER("initialize Producer ruby object\n");
928
+
929
+
930
+ topicPtr = StringValuePtr(topic);
931
+ brokersPtr = StringValuePtr(brokers);
932
+ Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
933
+
934
+ producerConfig->topic = topicPtr;
935
+ producerConfig->brokers = brokersPtr;
936
+ /** Using RD_KAFKA_PARTITION_UA specifies we want the partitioner callback to be called to determine the target
937
+ * partition
938
+ */
939
+ producerConfig->partition = RD_KAFKA_PARTITION_UA;
940
+ producerConfig->run = 1;
941
+ producerConfig->exit_eof = 0;
942
+ producerConfig->quiet = 0;
943
+
944
+ return self;
945
+ }
946
+
947
+ /**
948
+ * producer_init_copy
949
+ *
950
+ * Copy the configuration information from orig into copy for the given Producer instances.
951
+ *
952
+ * @param copy VALUE destination Producer
953
+ * @param orign VALUE source Producer
954
+ */
955
+ static VALUE producer_init_copy(VALUE copy,
956
+ VALUE orig) {
957
+ HermannInstanceConfig* orig_config;
958
+ HermannInstanceConfig* copy_config;
959
+
960
+ if (copy == orig) {
961
+ return copy;
962
+ }
963
+
964
+ if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)producer_free) {
965
+ rb_raise(rb_eTypeError, "wrong argument type");
966
+ }
967
+
968
+ Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
969
+ Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
970
+
971
+ // Copy over the data from one struct to the other
972
+ MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
973
+
974
+ return copy;
975
+ }
976
+
977
+ /**
978
+ * Init_hermann_lib
979
+ *
980
+ * Called by Ruby when the Hermann gem is loaded.
981
+ * Defines the Hermann module.
982
+ * Defines the Producer and Consumer classes.
983
+ */
984
+ void Init_hermann_lib() {
985
+ VALUE lib_module, c_consumer, c_producer;
986
+
987
+ TRACER("setting up Hermann::Lib\n");
988
+
989
+ /* Define the module */
990
+ hermann_module = rb_define_module("Hermann");
991
+ lib_module = rb_define_module_under(hermann_module, "Lib");
992
+
993
+
994
+ /* ---- Define the consumer class ---- */
995
+ c_consumer = rb_define_class_under(lib_module, "Consumer", rb_cObject);
996
+
997
+ /* Allocate */
998
+ rb_define_alloc_func(c_consumer, consumer_allocate);
999
+
1000
+ /* Initialize */
1001
+ rb_define_method(c_consumer, "initialize", consumer_initialize, 3);
1002
+ rb_define_method(c_consumer, "initialize_copy", consumer_init_copy, 1);
1003
+
1004
+ /* Consumer has method 'consume' */
1005
+ rb_define_method( c_consumer, "consume", consumer_consume, 0 );
1006
+
1007
+ /* ---- Define the producer class ---- */
1008
+ c_producer = rb_define_class_under(lib_module, "Producer", rb_cObject);
1009
+
1010
+ /* Allocate */
1011
+ rb_define_alloc_func(c_producer, producer_allocate);
1012
+
1013
+ /* Initialize */
1014
+ rb_define_method(c_producer, "initialize", producer_initialize, 2);
1015
+ rb_define_method(c_producer, "initialize_copy", producer_init_copy, 1);
1016
+
1017
+ /* Producer.push_single(msg) */
1018
+ rb_define_method(c_producer, "push_single", producer_push_single, 2);
1019
+
1020
+ /* Producer.tick */
1021
+ rb_define_method(c_producer, "tick", producer_tick, 1);
1022
+
1023
+ /* Producer.connected? */
1024
+ rb_define_method(c_producer, "connected?", producer_is_connected, 0);
1025
+
1026
+ /* Producer.errored? */
1027
+ rb_define_method(c_producer, "errored?", producer_is_errored, 0);
1028
+
1029
+ /* Producer.connect */
1030
+ rb_define_method(c_producer, "connect", producer_connect, 1);
1031
+ }