hermann 0.11-x86-darwin-12 → 0.15-x86-darwin-12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +17 -23
- data/ext/.ruby-version +1 -0
- data/ext/{extconf.rb → hermann/extconf.rb} +5 -3
- data/ext/hermann/hermann_lib.c +1029 -0
- data/ext/{hermann_lib.h → hermann/hermann_lib.h} +47 -22
- data/lib/hermann/consumer.rb +19 -0
- data/lib/hermann/errors.rb +8 -0
- data/lib/hermann/producer.rb +122 -0
- data/lib/hermann/result.rb +74 -0
- data/lib/hermann/timeout.rb +37 -0
- data/lib/hermann/version.rb +3 -0
- data/lib/hermann.rb +2 -1
- metadata +25 -18
- data/bin/hermann +0 -4
- data/ext/hermann_lib.c +0 -727
- data/ext/hermann_lib.o +0 -0
- data/test/test_hermann.rb +0 -9
@@ -0,0 +1,1029 @@
|
|
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 == strnlen(consumerConfig->topic, HERMANN_MAX_TOPIC_LEN))) {
|
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 == strnlen(producerConfig->topic, HERMANN_MAX_TOPIC_LEN))) {
|
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
|
+
/* Send/Produce message. */
|
577
|
+
if (-1 == rd_kafka_produce(producerConfig->rkt,
|
578
|
+
producerConfig->partition,
|
579
|
+
RD_KAFKA_MSG_F_COPY,
|
580
|
+
RSTRING_PTR(message),
|
581
|
+
RSTRING_LEN(message),
|
582
|
+
NULL,
|
583
|
+
0,
|
584
|
+
delivery_ctx)) {
|
585
|
+
fprintf(stderr, "%% Failed to produce to topic %s partition %i: %s\n",
|
586
|
+
rd_kafka_topic_name(producerConfig->rkt), producerConfig->partition,
|
587
|
+
rd_kafka_err2str(rd_kafka_errno2err(errno)));
|
588
|
+
/* TODO: raise a Ruby exception here, requires a test though */
|
589
|
+
}
|
590
|
+
|
591
|
+
TRACER("returning\n");
|
592
|
+
|
593
|
+
return self;
|
594
|
+
}
|
595
|
+
|
596
|
+
/**
|
597
|
+
* producer_tick
|
598
|
+
*
|
599
|
+
* This function is responsible for ticking the librdkafka reactor so we can
|
600
|
+
* get feedback from the librdkafka threads back into the Ruby environment
|
601
|
+
*
|
602
|
+
* @param self VALUE the Ruby producer instance
|
603
|
+
* @param message VALUE A Ruby FixNum of how many ms we should wait on librdkafka
|
604
|
+
*/
|
605
|
+
static VALUE producer_tick(VALUE self, VALUE timeout) {
|
606
|
+
hermann_conf_t *conf = NULL;
|
607
|
+
long timeout_ms = 0;
|
608
|
+
int events = 0;
|
609
|
+
|
610
|
+
if (Qnil != timeout) {
|
611
|
+
timeout_ms = rb_num2int(timeout);
|
612
|
+
}
|
613
|
+
else {
|
614
|
+
rb_raise(rb_eArgError, "Cannot call `tick` with a nil timeout!\n");
|
615
|
+
}
|
616
|
+
|
617
|
+
Data_Get_Struct(self, hermann_conf_t, conf);
|
618
|
+
|
619
|
+
/*
|
620
|
+
* if the producerConfig is not initialized then we never properly called
|
621
|
+
* producer_push_single, so why are we ticking?
|
622
|
+
*/
|
623
|
+
if (!conf->isInitialized) {
|
624
|
+
rb_raise(rb_eRuntimeError, "Cannot call `tick` without having ever sent a message\n");
|
625
|
+
}
|
626
|
+
|
627
|
+
events = rd_kafka_poll(conf->rk, timeout_ms);
|
628
|
+
|
629
|
+
if (conf->isErrored) {
|
630
|
+
rb_raise(rb_eStandardError, conf->error);
|
631
|
+
}
|
632
|
+
|
633
|
+
return rb_int_new(events);
|
634
|
+
}
|
635
|
+
|
636
|
+
|
637
|
+
static VALUE producer_connect(VALUE self, VALUE timeout) {
|
638
|
+
HermannInstanceConfig *producerConfig;
|
639
|
+
rd_kafka_resp_err_t err;
|
640
|
+
VALUE result = Qfalse;
|
641
|
+
int timeout_ms = rb_num2int(timeout);
|
642
|
+
struct rd_kafka_metadata *data = NULL;
|
643
|
+
|
644
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
645
|
+
|
646
|
+
if (!producerConfig->isInitialized) {
|
647
|
+
producer_init_kafka(self, producerConfig);
|
648
|
+
}
|
649
|
+
|
650
|
+
err = rd_kafka_metadata(producerConfig->rk,
|
651
|
+
0,
|
652
|
+
producerConfig->rkt,
|
653
|
+
&data,
|
654
|
+
timeout_ms);
|
655
|
+
TRACER("err: %s (%i)\n", rd_kafka_err2str(err), err);
|
656
|
+
|
657
|
+
if (RD_KAFKA_RESP_ERR_NO_ERROR == err) {
|
658
|
+
TRACER("brokers: %i, topics: %i\n",
|
659
|
+
data->broker_cnt,
|
660
|
+
data->topic_cnt);
|
661
|
+
producerConfig->isConnected = 1;
|
662
|
+
result = Qtrue;
|
663
|
+
}
|
664
|
+
else {
|
665
|
+
producerConfig->isErrored = err;
|
666
|
+
}
|
667
|
+
|
668
|
+
rd_kafka_metadata_destroy(data);
|
669
|
+
|
670
|
+
return result;
|
671
|
+
}
|
672
|
+
|
673
|
+
static VALUE producer_is_connected(VALUE self) {
|
674
|
+
HermannInstanceConfig *producerConfig;
|
675
|
+
|
676
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
677
|
+
|
678
|
+
if (!producerConfig->isInitialized) {
|
679
|
+
return Qfalse;
|
680
|
+
}
|
681
|
+
|
682
|
+
if (!producerConfig->isConnected) {
|
683
|
+
return Qfalse;
|
684
|
+
}
|
685
|
+
|
686
|
+
return Qtrue;
|
687
|
+
}
|
688
|
+
|
689
|
+
static VALUE producer_is_errored(VALUE self) {
|
690
|
+
HermannInstanceConfig *producerConfig;
|
691
|
+
|
692
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
693
|
+
|
694
|
+
if (producerConfig->isErrored) {
|
695
|
+
return Qtrue;
|
696
|
+
}
|
697
|
+
|
698
|
+
return Qfalse;
|
699
|
+
}
|
700
|
+
|
701
|
+
|
702
|
+
/**
|
703
|
+
* consumer_free
|
704
|
+
*
|
705
|
+
* Callback called when Ruby needs to GC the configuration associated with an Hermann instance.
|
706
|
+
*
|
707
|
+
* @param p void* the instance of an HermannInstanceConfig to be freed from allocated memory.
|
708
|
+
*/
|
709
|
+
static void consumer_free(void *p) {
|
710
|
+
|
711
|
+
HermannInstanceConfig* config = (HermannInstanceConfig *)p;
|
712
|
+
|
713
|
+
#ifdef TRACE
|
714
|
+
fprintf(stderr, "consumer_free\n");
|
715
|
+
#endif
|
716
|
+
|
717
|
+
// the p *should* contain a pointer to the consumerConfig which also must be freed
|
718
|
+
if (config->rkt != NULL) {
|
719
|
+
rd_kafka_topic_destroy(config->rkt);
|
720
|
+
}
|
721
|
+
|
722
|
+
if (config->rk != NULL) {
|
723
|
+
rd_kafka_destroy(config->rk);
|
724
|
+
}
|
725
|
+
|
726
|
+
// clean up the struct
|
727
|
+
free(config);
|
728
|
+
}
|
729
|
+
|
730
|
+
/**
|
731
|
+
* consumer_allocate
|
732
|
+
*
|
733
|
+
* Allocate and wrap an HermannInstanceConfig for this Consumer object.
|
734
|
+
*
|
735
|
+
* @param klass VALUE the class of the enclosing Ruby object.
|
736
|
+
*/
|
737
|
+
static VALUE consumer_allocate(VALUE klass) {
|
738
|
+
|
739
|
+
VALUE obj;
|
740
|
+
HermannInstanceConfig* consumerConfig;
|
741
|
+
|
742
|
+
#ifdef TRACE
|
743
|
+
fprintf(stderr, "consumer_free\n");
|
744
|
+
#endif
|
745
|
+
|
746
|
+
consumerConfig = ALLOC(HermannInstanceConfig);
|
747
|
+
|
748
|
+
// Make sure it's initialized
|
749
|
+
consumerConfig->topic = NULL;
|
750
|
+
consumerConfig->rk = NULL;
|
751
|
+
consumerConfig->rkt = NULL;
|
752
|
+
consumerConfig->brokers = NULL;
|
753
|
+
consumerConfig->partition = -1;
|
754
|
+
consumerConfig->topic_conf = NULL;
|
755
|
+
consumerConfig->errstr[0] = 0;
|
756
|
+
consumerConfig->conf = NULL;
|
757
|
+
consumerConfig->debug = NULL;
|
758
|
+
consumerConfig->start_offset = -1;
|
759
|
+
consumerConfig->do_conf_dump = -1;
|
760
|
+
consumerConfig->run = 0;
|
761
|
+
consumerConfig->exit_eof = 0;
|
762
|
+
consumerConfig->quiet = 0;
|
763
|
+
consumerConfig->isInitialized = 0;
|
764
|
+
|
765
|
+
obj = Data_Wrap_Struct(klass, 0, consumer_free, consumerConfig);
|
766
|
+
|
767
|
+
return obj;
|
768
|
+
}
|
769
|
+
|
770
|
+
/**
|
771
|
+
* consumer_initialize
|
772
|
+
*
|
773
|
+
* todo: configure the brokers through passed parameter, later through zk
|
774
|
+
*
|
775
|
+
* Set up the Consumer's HermannInstanceConfig context.
|
776
|
+
*
|
777
|
+
* @param self VALUE the Ruby instance of the Consumer
|
778
|
+
* @param topic VALUE a Ruby string
|
779
|
+
* @param brokers VALUE a Ruby string containing list of host:port
|
780
|
+
* @param partition VALUE a Ruby number
|
781
|
+
*/
|
782
|
+
static VALUE consumer_initialize(VALUE self,
|
783
|
+
VALUE topic,
|
784
|
+
VALUE brokers,
|
785
|
+
VALUE partition) {
|
786
|
+
|
787
|
+
HermannInstanceConfig* consumerConfig;
|
788
|
+
char* topicPtr;
|
789
|
+
char* brokersPtr;
|
790
|
+
int partitionNo;
|
791
|
+
|
792
|
+
TRACER("initing consumer ruby object\n");
|
793
|
+
|
794
|
+
topicPtr = StringValuePtr(topic);
|
795
|
+
brokersPtr = StringValuePtr(brokers);
|
796
|
+
partitionNo = FIX2INT(partition);
|
797
|
+
Data_Get_Struct(self, HermannInstanceConfig, consumerConfig);
|
798
|
+
|
799
|
+
consumerConfig->topic = topicPtr;
|
800
|
+
consumerConfig->brokers = brokersPtr;
|
801
|
+
consumerConfig->partition = partitionNo;
|
802
|
+
consumerConfig->run = 1;
|
803
|
+
consumerConfig->exit_eof = 0;
|
804
|
+
consumerConfig->quiet = 0;
|
805
|
+
|
806
|
+
return self;
|
807
|
+
}
|
808
|
+
|
809
|
+
/**
|
810
|
+
* consumer_init_copy
|
811
|
+
*
|
812
|
+
* When copying into a new instance of a Consumer, reproduce the configuration info.
|
813
|
+
*
|
814
|
+
* @param copy VALUE the Ruby Consumer instance (with configuration) as destination
|
815
|
+
* @param orig VALUE the Ruby Consumer instance (with configuration) as source
|
816
|
+
*
|
817
|
+
*/
|
818
|
+
static VALUE consumer_init_copy(VALUE copy,
|
819
|
+
VALUE orig) {
|
820
|
+
HermannInstanceConfig* orig_config;
|
821
|
+
HermannInstanceConfig* copy_config;
|
822
|
+
|
823
|
+
if (copy == orig) {
|
824
|
+
return copy;
|
825
|
+
}
|
826
|
+
|
827
|
+
if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)consumer_free) {
|
828
|
+
rb_raise(rb_eTypeError, "wrong argument type");
|
829
|
+
}
|
830
|
+
|
831
|
+
Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
|
832
|
+
Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
|
833
|
+
|
834
|
+
// Copy over the data from one struct to the other
|
835
|
+
MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
|
836
|
+
|
837
|
+
return copy;
|
838
|
+
}
|
839
|
+
|
840
|
+
/**
|
841
|
+
* producer_free
|
842
|
+
*
|
843
|
+
* Reclaim memory allocated to the Producer's configuration
|
844
|
+
*
|
845
|
+
* @param p void* the instance's configuration struct
|
846
|
+
*/
|
847
|
+
static void producer_free(void *p) {
|
848
|
+
|
849
|
+
HermannInstanceConfig* config = (HermannInstanceConfig *)p;
|
850
|
+
|
851
|
+
TRACER("dealloc producer ruby object (%p)\n", p);
|
852
|
+
|
853
|
+
|
854
|
+
if (NULL == p) {
|
855
|
+
return;
|
856
|
+
}
|
857
|
+
|
858
|
+
// Clean up the topic
|
859
|
+
if (NULL != config->rkt) {
|
860
|
+
rd_kafka_topic_destroy(config->rkt);
|
861
|
+
}
|
862
|
+
|
863
|
+
// Take care of the producer instance
|
864
|
+
if (NULL != config->rk) {
|
865
|
+
rd_kafka_destroy(config->rk);
|
866
|
+
}
|
867
|
+
|
868
|
+
// Free the struct
|
869
|
+
free(config);
|
870
|
+
}
|
871
|
+
|
872
|
+
/**
|
873
|
+
* producer_allocate
|
874
|
+
*
|
875
|
+
* Allocate the memory for a Producer's configuration
|
876
|
+
*
|
877
|
+
* @param klass VALUE the class of the Producer
|
878
|
+
*/
|
879
|
+
static VALUE producer_allocate(VALUE klass) {
|
880
|
+
|
881
|
+
VALUE obj;
|
882
|
+
HermannInstanceConfig* producerConfig = ALLOC(HermannInstanceConfig);
|
883
|
+
|
884
|
+
producerConfig->topic = NULL;
|
885
|
+
producerConfig->rk = NULL;
|
886
|
+
producerConfig->rkt = NULL;
|
887
|
+
producerConfig->brokers = NULL;
|
888
|
+
producerConfig->partition = -1;
|
889
|
+
producerConfig->topic_conf = NULL;
|
890
|
+
producerConfig->errstr[0] = 0;
|
891
|
+
producerConfig->conf = NULL;
|
892
|
+
producerConfig->debug = NULL;
|
893
|
+
producerConfig->start_offset = -1;
|
894
|
+
producerConfig->do_conf_dump = -1;
|
895
|
+
producerConfig->run = 0;
|
896
|
+
producerConfig->exit_eof = 0;
|
897
|
+
producerConfig->quiet = 0;
|
898
|
+
producerConfig->isInitialized = 0;
|
899
|
+
producerConfig->isConnected = 0;
|
900
|
+
producerConfig->isErrored = 0;
|
901
|
+
producerConfig->error = NULL;
|
902
|
+
|
903
|
+
obj = Data_Wrap_Struct(klass, 0, producer_free, producerConfig);
|
904
|
+
|
905
|
+
return obj;
|
906
|
+
}
|
907
|
+
|
908
|
+
/**
|
909
|
+
* producer_initialize
|
910
|
+
*
|
911
|
+
* Set up the configuration context for the Producer instance
|
912
|
+
*
|
913
|
+
* @param self VALUE the Producer instance
|
914
|
+
* @param topic VALUE the Ruby string naming the topic
|
915
|
+
* @param brokers VALUE a Ruby string containing host:port pairs separated by commas
|
916
|
+
*/
|
917
|
+
static VALUE producer_initialize(VALUE self,
|
918
|
+
VALUE topic,
|
919
|
+
VALUE brokers) {
|
920
|
+
|
921
|
+
HermannInstanceConfig* producerConfig;
|
922
|
+
char* topicPtr;
|
923
|
+
char* brokersPtr;
|
924
|
+
|
925
|
+
TRACER("initialize Producer ruby object\n");
|
926
|
+
|
927
|
+
|
928
|
+
topicPtr = StringValuePtr(topic);
|
929
|
+
brokersPtr = StringValuePtr(brokers);
|
930
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
931
|
+
|
932
|
+
producerConfig->topic = topicPtr;
|
933
|
+
producerConfig->brokers = brokersPtr;
|
934
|
+
/** Using RD_KAFKA_PARTITION_UA specifies we want the partitioner callback to be called to determine the target
|
935
|
+
* partition
|
936
|
+
*/
|
937
|
+
producerConfig->partition = RD_KAFKA_PARTITION_UA;
|
938
|
+
producerConfig->run = 1;
|
939
|
+
producerConfig->exit_eof = 0;
|
940
|
+
producerConfig->quiet = 0;
|
941
|
+
|
942
|
+
return self;
|
943
|
+
}
|
944
|
+
|
945
|
+
/**
|
946
|
+
* producer_init_copy
|
947
|
+
*
|
948
|
+
* Copy the configuration information from orig into copy for the given Producer instances.
|
949
|
+
*
|
950
|
+
* @param copy VALUE destination Producer
|
951
|
+
* @param orig VALUE source Producer
|
952
|
+
*/
|
953
|
+
static VALUE producer_init_copy(VALUE copy,
|
954
|
+
VALUE orig) {
|
955
|
+
HermannInstanceConfig* orig_config;
|
956
|
+
HermannInstanceConfig* copy_config;
|
957
|
+
|
958
|
+
if (copy == orig) {
|
959
|
+
return copy;
|
960
|
+
}
|
961
|
+
|
962
|
+
if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)producer_free) {
|
963
|
+
rb_raise(rb_eTypeError, "wrong argument type");
|
964
|
+
}
|
965
|
+
|
966
|
+
Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
|
967
|
+
Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
|
968
|
+
|
969
|
+
// Copy over the data from one struct to the other
|
970
|
+
MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
|
971
|
+
|
972
|
+
return copy;
|
973
|
+
}
|
974
|
+
|
975
|
+
/**
|
976
|
+
* Init_hermann_lib
|
977
|
+
*
|
978
|
+
* Called by Ruby when the Hermann gem is loaded.
|
979
|
+
* Defines the Hermann module.
|
980
|
+
* Defines the Producer and Consumer classes.
|
981
|
+
*/
|
982
|
+
void Init_hermann_lib() {
|
983
|
+
VALUE lib_module, c_consumer, c_producer;
|
984
|
+
|
985
|
+
TRACER("setting up Hermann::Lib\n");
|
986
|
+
|
987
|
+
/* Define the module */
|
988
|
+
hermann_module = rb_define_module("Hermann");
|
989
|
+
lib_module = rb_define_module_under(hermann_module, "Lib");
|
990
|
+
|
991
|
+
|
992
|
+
/* ---- Define the consumer class ---- */
|
993
|
+
c_consumer = rb_define_class_under(lib_module, "Consumer", rb_cObject);
|
994
|
+
|
995
|
+
/* Allocate */
|
996
|
+
rb_define_alloc_func(c_consumer, consumer_allocate);
|
997
|
+
|
998
|
+
/* Initialize */
|
999
|
+
rb_define_method(c_consumer, "initialize", consumer_initialize, 3);
|
1000
|
+
rb_define_method(c_consumer, "initialize_copy", consumer_init_copy, 1);
|
1001
|
+
|
1002
|
+
/* Consumer has method 'consume' */
|
1003
|
+
rb_define_method( c_consumer, "consume", consumer_consume, 0 );
|
1004
|
+
|
1005
|
+
/* ---- Define the producer class ---- */
|
1006
|
+
c_producer = rb_define_class_under(lib_module, "Producer", rb_cObject);
|
1007
|
+
|
1008
|
+
/* Allocate */
|
1009
|
+
rb_define_alloc_func(c_producer, producer_allocate);
|
1010
|
+
|
1011
|
+
/* Initialize */
|
1012
|
+
rb_define_method(c_producer, "initialize", producer_initialize, 2);
|
1013
|
+
rb_define_method(c_producer, "initialize_copy", producer_init_copy, 1);
|
1014
|
+
|
1015
|
+
/* Producer.push_single(msg) */
|
1016
|
+
rb_define_method(c_producer, "push_single", producer_push_single, 2);
|
1017
|
+
|
1018
|
+
/* Producer.tick */
|
1019
|
+
rb_define_method(c_producer, "tick", producer_tick, 1);
|
1020
|
+
|
1021
|
+
/* Producer.connected? */
|
1022
|
+
rb_define_method(c_producer, "connected?", producer_is_connected, 0);
|
1023
|
+
|
1024
|
+
/* Producer.errored? */
|
1025
|
+
rb_define_method(c_producer, "errored?", producer_is_errored, 0);
|
1026
|
+
|
1027
|
+
/* Producer.connect */
|
1028
|
+
rb_define_method(c_producer, "connect", producer_connect, 1);
|
1029
|
+
}
|