hermann 0.11-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 +31 -0
- data/bin/hermann +4 -0
- data/ext/extconf.rb +32 -0
- data/ext/hermann_lib.c +727 -0
- data/ext/hermann_lib.h +83 -0
- data/ext/hermann_lib.o +0 -0
- data/lib/hermann.rb +1 -0
- data/test/test_hermann.rb +9 -0
- metadata +72 -0
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require 'rake/clean'
|
3
|
+
|
4
|
+
EXT_CONF = "ext/extconf.rb"
|
5
|
+
MAKEFILE = 'ext/Makefile'
|
6
|
+
MODULE = 'ext/hermann_lib.so'
|
7
|
+
SRC = Dir.glob('ext/*.c')
|
8
|
+
SRC << MAKEFILE
|
9
|
+
|
10
|
+
CLEAN.include [ 'ext/*.o', 'ext/depend', 'ext/hermann_lib.bundle', MODULE ]
|
11
|
+
CLOBBER.include [ 'config.save', 'ext/mkmf.log', 'ext/hermann_lib.bundle', MAKEFILE ]
|
12
|
+
|
13
|
+
file MAKEFILE => EXT_CONF do |t|
|
14
|
+
Dir::chdir(File::dirname(EXT_CONF)) do
|
15
|
+
unless sh "ruby #{File::basename(EXT_CONF)}"
|
16
|
+
$stderr.puts "Failed to run extconf"
|
17
|
+
break
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
file MODULE => SRC do |t|
|
23
|
+
Dir::chdir(File::dirname(EXT_CONF)) do
|
24
|
+
unless sh "make"
|
25
|
+
$stderr.puts "make failed"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Build the native library"
|
31
|
+
task :build => MODULE
|
data/bin/hermann
ADDED
data/ext/extconf.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# External configuration for Hermann Gem
|
2
|
+
|
3
|
+
require 'mkmf'
|
4
|
+
|
5
|
+
RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC']
|
6
|
+
|
7
|
+
LIBDIR = RbConfig::CONFIG['libdir']
|
8
|
+
INCLUDEDIR = RbConfig::CONFIG['includedir']
|
9
|
+
|
10
|
+
puts "Library Dir: #{LIBDIR}\n"
|
11
|
+
puts "Include Dir: #{INCLUDEDIR}"
|
12
|
+
|
13
|
+
HEADER_DIRS = [INCLUDEDIR]
|
14
|
+
|
15
|
+
LIB_DIRS = [LIBDIR]
|
16
|
+
|
17
|
+
dir_config('rdkafka', HEADER_DIRS, LIB_DIRS)
|
18
|
+
|
19
|
+
unless find_header('librdkafka/rdkafka.h')
|
20
|
+
abort "librdkafka not installed"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless find_library('rdkafka', 'rd_kafka_conf_new')
|
24
|
+
abort "librdkafka not installed"
|
25
|
+
end
|
26
|
+
|
27
|
+
#unless have_func "rb_thread_blocking_region"
|
28
|
+
# abort "rb_thread_blocking_region function missing"
|
29
|
+
#end
|
30
|
+
|
31
|
+
# create_header('hermann_lib.h')
|
32
|
+
create_makefile('hermann/hermann_lib')
|
data/ext/hermann_lib.c
ADDED
@@ -0,0 +1,727 @@
|
|
1
|
+
/*
|
2
|
+
* hermann_lib.c - Ruby wrapper for the librdkafka library
|
3
|
+
*
|
4
|
+
* Copyright (c) 2014 Stan Campbell
|
5
|
+
* All rights reserved.
|
6
|
+
*
|
7
|
+
* Redistribution and use in source and binary forms, with or without
|
8
|
+
* modification, are permitted provided that the following conditions are met:
|
9
|
+
*
|
10
|
+
* 1. Redistributions of source code must retain the above copyright notice,
|
11
|
+
* this list of conditions and the following disclaimer.
|
12
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
* this list of conditions and the following disclaimer in the documentation
|
14
|
+
* and/or other materials provided with the distribution.
|
15
|
+
*
|
16
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
17
|
+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
18
|
+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
19
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
20
|
+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
21
|
+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
22
|
+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
23
|
+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
24
|
+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
25
|
+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
26
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
*/
|
28
|
+
|
29
|
+
/* Much of the librdkafka library calls were lifted from rdkafka_example.c */
|
30
|
+
|
31
|
+
#include "hermann_lib.h"
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Utility functions
|
35
|
+
*/
|
36
|
+
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Convenience function
|
40
|
+
*
|
41
|
+
* @param msg char* the string to be logged under debugging.
|
42
|
+
*/
|
43
|
+
void log_debug(char* msg) {
|
44
|
+
if(DEBUG) {
|
45
|
+
fprintf(stderr, "%s\n", msg);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Message delivery report callback.
|
51
|
+
* Called once for each message.
|
52
|
+
*
|
53
|
+
* @param rk rd_kafka_t* instance of producer or consumer
|
54
|
+
* @param payload void* the payload of the message
|
55
|
+
* @param len size_t the length of the payload in bytes
|
56
|
+
* @param error_code int
|
57
|
+
* @param opaque void* optional context
|
58
|
+
* @param msg_opaque void* it's opaque
|
59
|
+
*/
|
60
|
+
static void msg_delivered (rd_kafka_t *rk,
|
61
|
+
void *payload, size_t len,
|
62
|
+
int error_code,
|
63
|
+
void *opaque, void *msg_opaque) {
|
64
|
+
|
65
|
+
if (error_code)
|
66
|
+
fprintf(stderr, "%% Message delivery failed: %s\n",
|
67
|
+
rd_kafka_err2str(error_code));
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Producer partitioner callback.
|
72
|
+
* Used to determine the target partition within a topic for production.
|
73
|
+
*
|
74
|
+
* Returns an integer partition number or RD_KAFKA_PARTITION_UA if no
|
75
|
+
* available partition could be determined.
|
76
|
+
*
|
77
|
+
* @param rkt rd_kafka_topic_t* the topic
|
78
|
+
* @param keydata void* key information for calculating the partition
|
79
|
+
* @param keylen size_t key size
|
80
|
+
* @param partition_cnt int32_t the count of the number of partitions
|
81
|
+
* @param rkt_opaque void* opaque topic info
|
82
|
+
* @param msg_opaque void* opaque message info
|
83
|
+
*/
|
84
|
+
static int32_t producer_paritioner_callback( const rd_kafka_topic_t *rkt,
|
85
|
+
const void *keydata,
|
86
|
+
size_t keylen,
|
87
|
+
int32_t partition_cnt,
|
88
|
+
void *rkt_opaque,
|
89
|
+
void *msg_opaque) {
|
90
|
+
/* Pick a random partition */
|
91
|
+
int retry;
|
92
|
+
for(retry=0;retry<partition_cnt;retry++) {
|
93
|
+
int32_t partition = rand() % partition_cnt;
|
94
|
+
if(rd_kafka_topic_partition_available(rkt, partition)) {
|
95
|
+
break; /* this one will do */
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
/**
|
101
|
+
* hexdump
|
102
|
+
*
|
103
|
+
* Write the given payload to file in hex notation.
|
104
|
+
*
|
105
|
+
* @param fp FILE* the file into which to write
|
106
|
+
* @param name char* name
|
107
|
+
* @param ptr void* payload
|
108
|
+
* @param len size_t payload length
|
109
|
+
*/
|
110
|
+
static void hexdump (FILE *fp, const char *name, const void *ptr, size_t len) {
|
111
|
+
const char *p = (const char *)ptr;
|
112
|
+
int of = 0;
|
113
|
+
|
114
|
+
|
115
|
+
if (name)
|
116
|
+
fprintf(fp, "%s hexdump (%zd bytes):\n", name, len);
|
117
|
+
|
118
|
+
for (of = 0 ; of < len ; of += 16) {
|
119
|
+
char hexen[16*3+1];
|
120
|
+
char charen[16+1];
|
121
|
+
int hof = 0;
|
122
|
+
|
123
|
+
int cof = 0;
|
124
|
+
int i;
|
125
|
+
|
126
|
+
for (i = of ; i < of + 16 && i < len ; i++) {
|
127
|
+
hof += sprintf(hexen+hof, "%02x ", p[i] & 0xff);
|
128
|
+
cof += sprintf(charen+cof, "%c",
|
129
|
+
isprint((int)p[i]) ? p[i] : '.');
|
130
|
+
}
|
131
|
+
fprintf(fp, "%08x: %-48s %-16s\n",
|
132
|
+
of, hexen, charen);
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* msg_consume
|
138
|
+
*
|
139
|
+
* Callback on message receipt.
|
140
|
+
*
|
141
|
+
* @param rkmessage rd_kafka_message_t* the message
|
142
|
+
* @param opaque void* opaque context
|
143
|
+
*/
|
144
|
+
static void msg_consume (rd_kafka_message_t *rkmessage,
|
145
|
+
void *opaque) {
|
146
|
+
|
147
|
+
HermannInstanceConfig* cfg;
|
148
|
+
|
149
|
+
cfg = (HermannInstanceConfig*)opaque;
|
150
|
+
|
151
|
+
if (rkmessage->err) {
|
152
|
+
if (rkmessage->err == RD_KAFKA_RESP_ERR__PARTITION_EOF) {
|
153
|
+
fprintf(stderr,
|
154
|
+
"%% Consumer reached end of %s [%"PRId32"] "
|
155
|
+
"message queue at offset %"PRId64"\n",
|
156
|
+
rd_kafka_topic_name(rkmessage->rkt),
|
157
|
+
rkmessage->partition, rkmessage->offset);
|
158
|
+
|
159
|
+
if (cfg->exit_eof)
|
160
|
+
cfg->run = 0;
|
161
|
+
|
162
|
+
return;
|
163
|
+
}
|
164
|
+
|
165
|
+
fprintf(stderr, "%% Consume error for topic \"%s\" [%"PRId32"] "
|
166
|
+
"offset %"PRId64": %s\n",
|
167
|
+
rd_kafka_topic_name(rkmessage->rkt),
|
168
|
+
rkmessage->partition,
|
169
|
+
rkmessage->offset,
|
170
|
+
rd_kafka_message_errstr(rkmessage));
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
|
174
|
+
if (DEBUG && rkmessage->key_len) {
|
175
|
+
if (output == OUTPUT_HEXDUMP)
|
176
|
+
hexdump(stdout, "Message Key",
|
177
|
+
rkmessage->key, rkmessage->key_len);
|
178
|
+
else
|
179
|
+
printf("Key: %.*s\n",
|
180
|
+
(int)rkmessage->key_len, (char *)rkmessage->key);
|
181
|
+
}
|
182
|
+
|
183
|
+
if (output == OUTPUT_HEXDUMP) {
|
184
|
+
if(DEBUG)
|
185
|
+
hexdump(stdout, "Message Payload", rkmessage->payload, rkmessage->len);
|
186
|
+
} else {
|
187
|
+
if(DEBUG)
|
188
|
+
printf("%.*s\n", (int)rkmessage->len, (char *)rkmessage->payload);
|
189
|
+
}
|
190
|
+
|
191
|
+
// Yield the data to the Consumer's block
|
192
|
+
if(rb_block_given_p()) {
|
193
|
+
VALUE value = rb_str_new((char *)rkmessage->payload, rkmessage->len);
|
194
|
+
rb_yield(value);
|
195
|
+
} else {
|
196
|
+
if(DEBUG)
|
197
|
+
fprintf(stderr, "No block given\n"); // todo: should this be an error?
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
/**
|
202
|
+
* logger
|
203
|
+
*
|
204
|
+
* Kafka logger callback (optional)
|
205
|
+
*
|
206
|
+
* todo: introduce better logging
|
207
|
+
*
|
208
|
+
* @param rk rd_kafka_t the producer or consumer
|
209
|
+
* @param level int the log level
|
210
|
+
* @param fac char* something of which I am unaware
|
211
|
+
* @param buf char* the log message
|
212
|
+
*/
|
213
|
+
static void logger (const rd_kafka_t *rk, int level,
|
214
|
+
const char *fac, const char *buf) {
|
215
|
+
struct timeval tv;
|
216
|
+
gettimeofday(&tv, NULL);
|
217
|
+
fprintf(stderr, "%u.%03u RDKAFKA-%i-%s: %s: %s\n",
|
218
|
+
(int)tv.tv_sec, (int)(tv.tv_usec / 1000),
|
219
|
+
level, fac, rd_kafka_name(rk), buf);
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
* consumer_init_kafka
|
224
|
+
*
|
225
|
+
* Initialize the Kafka context and instantiate a consumer.
|
226
|
+
*
|
227
|
+
* @param config HermannInstanceConfig* pointer to the instance configuration for this producer or consumer
|
228
|
+
*/
|
229
|
+
void consumer_init_kafka(HermannInstanceConfig* config) {
|
230
|
+
|
231
|
+
config->quiet = !isatty(STDIN_FILENO);
|
232
|
+
|
233
|
+
/* Kafka configuration */
|
234
|
+
config->conf = rd_kafka_conf_new();
|
235
|
+
|
236
|
+
/* Topic configuration */
|
237
|
+
config->topic_conf = rd_kafka_topic_conf_new();
|
238
|
+
|
239
|
+
/* Create Kafka handle */
|
240
|
+
if (!(config->rk = rd_kafka_new(RD_KAFKA_CONSUMER, config->conf,
|
241
|
+
config->errstr, sizeof(config->errstr)))) {
|
242
|
+
fprintf(stderr, "%% Failed to create new consumer: %s\n", config->errstr);
|
243
|
+
exit(1);
|
244
|
+
}
|
245
|
+
|
246
|
+
/* Set logger */
|
247
|
+
rd_kafka_set_logger(config->rk, logger);
|
248
|
+
rd_kafka_set_log_level(config->rk, LOG_DEBUG);
|
249
|
+
|
250
|
+
/* TODO: offset calculation */
|
251
|
+
config->start_offset = RD_KAFKA_OFFSET_END;
|
252
|
+
|
253
|
+
/* Add brokers */
|
254
|
+
if(rd_kafka_brokers_add(config->rk, config->brokers) == 0) {
|
255
|
+
fprintf(stderr, "%% No valid brokers specified\n");
|
256
|
+
exit(1);
|
257
|
+
}
|
258
|
+
|
259
|
+
/* Create topic */
|
260
|
+
config->rkt = rd_kafka_topic_new(config->rk, config->topic, config->topic_conf);
|
261
|
+
|
262
|
+
/* We're now initialized */
|
263
|
+
config->isInitialized = 1;
|
264
|
+
}
|
265
|
+
|
266
|
+
// Ruby gem extensions
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Callback invoked if Ruby needs to stop our Consumer's IO loop for any reason (system exit, etc.)
|
270
|
+
*/
|
271
|
+
static void consumer_consume_stop_callback(void *ptr) {
|
272
|
+
HermannInstanceConfig* config = (HermannInstanceConfig*)ptr;
|
273
|
+
|
274
|
+
config->run = 0;
|
275
|
+
}
|
276
|
+
|
277
|
+
/**
|
278
|
+
* Loop on a timeout to receive messages from Kafka. When the consumer_consume_stop_callback is invoked by Ruby,
|
279
|
+
* we'll break out of our loop and return.
|
280
|
+
*/
|
281
|
+
void consumer_consume_loop(HermannInstanceConfig* consumerConfig) {
|
282
|
+
|
283
|
+
while (consumerConfig->run) {
|
284
|
+
rd_kafka_message_t *rkmessage;
|
285
|
+
|
286
|
+
if(rd_kafka_consume_callback(consumerConfig->rkt, consumerConfig->partition,
|
287
|
+
1000/*timeout*/,
|
288
|
+
msg_consume,
|
289
|
+
consumerConfig) < 0) {
|
290
|
+
fprintf(stderr, "%% Error: %s\n", rd_kafka_err2str( rd_kafka_errno2err(errno)));
|
291
|
+
}
|
292
|
+
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
/**
|
297
|
+
* Hermann::Consumer.consume
|
298
|
+
*
|
299
|
+
* Begin listening on the configured topic for messages. msg_consume will be called on each message received.
|
300
|
+
*
|
301
|
+
* @param VALUE self the Ruby object for this consumer
|
302
|
+
*/
|
303
|
+
static VALUE consumer_consume(VALUE self) {
|
304
|
+
|
305
|
+
HermannInstanceConfig* consumerConfig;
|
306
|
+
|
307
|
+
Data_Get_Struct(self, HermannInstanceConfig, consumerConfig);
|
308
|
+
|
309
|
+
if(consumerConfig->topic==NULL) {
|
310
|
+
fprintf(stderr, "Topic is null!");
|
311
|
+
return;
|
312
|
+
}
|
313
|
+
|
314
|
+
if(!consumerConfig->isInitialized) {
|
315
|
+
consumer_init_kafka(consumerConfig);
|
316
|
+
}
|
317
|
+
|
318
|
+
/* Start consuming */
|
319
|
+
if (rd_kafka_consume_start(consumerConfig->rkt, consumerConfig->partition, consumerConfig->start_offset) == -1){
|
320
|
+
fprintf(stderr, "%% Failed to start consuming: %s\n",
|
321
|
+
rd_kafka_err2str(rd_kafka_errno2err(errno)));
|
322
|
+
exit(1);
|
323
|
+
}
|
324
|
+
|
325
|
+
#ifdef RB_THREAD_BLOCKING_REGION
|
326
|
+
/** The consumer will listen for incoming messages in a loop, timing out and checking the consumerConfig->run
|
327
|
+
* flag every second.
|
328
|
+
*
|
329
|
+
* Call rb_thread_blocking_region to release the GVM lock and allow Ruby to amuse itself while we wait on
|
330
|
+
* IO from Kafka.
|
331
|
+
*
|
332
|
+
* If Ruby needs to interrupt the consumer loop, the stop callback will be invoked and the loop should exit.
|
333
|
+
*/
|
334
|
+
rb_thread_blocking_region(consumer_consume_loop, consumerConfig, consumer_consume_stop_callback, consumerConfig);
|
335
|
+
#else
|
336
|
+
consumer_consume_loop(consumerConfig);
|
337
|
+
#endif
|
338
|
+
|
339
|
+
|
340
|
+
/* Stop consuming */
|
341
|
+
rd_kafka_consume_stop(consumerConfig->rkt, consumerConfig->partition);
|
342
|
+
|
343
|
+
return Qnil;
|
344
|
+
}
|
345
|
+
|
346
|
+
/**
|
347
|
+
* producer_init_kafka
|
348
|
+
*
|
349
|
+
* Initialize the producer instance, setting up the Kafka topic and context.
|
350
|
+
*
|
351
|
+
* @param config HermannInstanceConfig* the instance configuration associated with this producer.
|
352
|
+
*/
|
353
|
+
void producer_init_kafka(HermannInstanceConfig* config) {
|
354
|
+
|
355
|
+
config->quiet = !isatty(STDIN_FILENO);
|
356
|
+
|
357
|
+
/* Kafka configuration */
|
358
|
+
config->conf = rd_kafka_conf_new();
|
359
|
+
|
360
|
+
/* Topic configuration */
|
361
|
+
config->topic_conf = rd_kafka_topic_conf_new();
|
362
|
+
|
363
|
+
/* Set up a message delivery report callback.
|
364
|
+
* It will be called once for each message, either on successful
|
365
|
+
* delivery to broker, or upon failure to deliver to broker. */
|
366
|
+
rd_kafka_conf_set_dr_cb(config->conf, msg_delivered);
|
367
|
+
|
368
|
+
/* Create Kafka handle */
|
369
|
+
if (!(config->rk = rd_kafka_new(RD_KAFKA_PRODUCER, config->conf, config->errstr, sizeof(config->errstr)))) {
|
370
|
+
fprintf(stderr,
|
371
|
+
"%% Failed to create new producer: %s\n", config->errstr);
|
372
|
+
exit(1);
|
373
|
+
}
|
374
|
+
|
375
|
+
/* Set logger */
|
376
|
+
rd_kafka_set_logger(config->rk, logger);
|
377
|
+
rd_kafka_set_log_level(config->rk, LOG_DEBUG);
|
378
|
+
|
379
|
+
if(rd_kafka_brokers_add(config->rk, config->brokers) == 0) {
|
380
|
+
fprintf(stderr, "%% No valid brokers specified\n");
|
381
|
+
exit(1);
|
382
|
+
}
|
383
|
+
|
384
|
+
/* Create topic */
|
385
|
+
config->rkt = rd_kafka_topic_new(config->rk, config->topic, config->topic_conf);
|
386
|
+
|
387
|
+
/* Set the partitioner callback */
|
388
|
+
rd_kafka_topic_conf_set_partitioner_cb( config->topic_conf, producer_paritioner_callback );
|
389
|
+
|
390
|
+
/* We're now initialized */
|
391
|
+
config->isInitialized = 1;
|
392
|
+
}
|
393
|
+
|
394
|
+
/**
|
395
|
+
* producer_push_single
|
396
|
+
*
|
397
|
+
* @param self VALUE the Ruby producer instance
|
398
|
+
* @param message VALUE the ruby String containing the outgoing message.
|
399
|
+
*/
|
400
|
+
static VALUE producer_push_single(VALUE self, VALUE message) {
|
401
|
+
|
402
|
+
HermannInstanceConfig* producerConfig;
|
403
|
+
char buf[2048];
|
404
|
+
|
405
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
406
|
+
|
407
|
+
if(producerConfig->topic==NULL) {
|
408
|
+
fprintf(stderr, "Topic is null!");
|
409
|
+
return self;
|
410
|
+
}
|
411
|
+
|
412
|
+
if(!producerConfig->isInitialized) {
|
413
|
+
producer_init_kafka(producerConfig);
|
414
|
+
}
|
415
|
+
|
416
|
+
char *msg = StringValueCStr(message);
|
417
|
+
strcpy(buf, msg);
|
418
|
+
|
419
|
+
size_t len = strlen(buf);
|
420
|
+
if (buf[len-1] == '\n')
|
421
|
+
buf[--len] = '\0';
|
422
|
+
|
423
|
+
/* Send/Produce message. */
|
424
|
+
if (rd_kafka_produce(producerConfig->rkt, producerConfig->partition, RD_KAFKA_MSG_F_COPY,
|
425
|
+
/* Payload and length */
|
426
|
+
buf, len,
|
427
|
+
/* Optional key and its length */
|
428
|
+
NULL, 0,
|
429
|
+
/* Message opaque, provided in
|
430
|
+
* delivery report callback as
|
431
|
+
* msg_opaque. */
|
432
|
+
NULL) == -1) {
|
433
|
+
|
434
|
+
fprintf(stderr, "%% Failed to produce to topic %s partition %i: %s\n",
|
435
|
+
rd_kafka_topic_name(producerConfig->rkt), producerConfig->partition,
|
436
|
+
rd_kafka_err2str(rd_kafka_errno2err(errno)));
|
437
|
+
|
438
|
+
/* Poll to handle delivery reports */
|
439
|
+
rd_kafka_poll(producerConfig->rk, 10);
|
440
|
+
}
|
441
|
+
|
442
|
+
/* Must poll to handle delivery reports */
|
443
|
+
rd_kafka_poll(producerConfig->rk, 0);
|
444
|
+
|
445
|
+
return self;
|
446
|
+
}
|
447
|
+
|
448
|
+
/**
|
449
|
+
* producer_push_array
|
450
|
+
*
|
451
|
+
* Publish each of the messages in array on the configured topic.
|
452
|
+
*
|
453
|
+
* @param self VALUE the instance of the Ruby Producer object
|
454
|
+
* @param length int the length of the outgoing messages array
|
455
|
+
* @param array VALUE the Ruby array of messages
|
456
|
+
*/
|
457
|
+
static VALUE producer_push_array(VALUE self, int length, VALUE array) {
|
458
|
+
|
459
|
+
int i;
|
460
|
+
VALUE message;
|
461
|
+
|
462
|
+
for(i=0;i<length;i++) {
|
463
|
+
message = RARRAY_PTR(array)[i];
|
464
|
+
producer_push_single(self, message);
|
465
|
+
}
|
466
|
+
|
467
|
+
return self;
|
468
|
+
}
|
469
|
+
|
470
|
+
/**
|
471
|
+
* Hermann::Producer.push(msg)
|
472
|
+
*
|
473
|
+
* Publish the given message on the configured topic.
|
474
|
+
*
|
475
|
+
* @param self VALUE the Ruby instance of the Producer.
|
476
|
+
* @param message VALUE the Ruby string containing the message.
|
477
|
+
*/
|
478
|
+
static VALUE producer_push(VALUE self, VALUE message) {
|
479
|
+
|
480
|
+
VALUE arrayP = rb_check_array_type(message);
|
481
|
+
|
482
|
+
if(!NIL_P(arrayP)) {
|
483
|
+
return producer_push_array(self, RARRAY_LEN(arrayP), message);
|
484
|
+
} else {
|
485
|
+
return producer_push_single(self, message);
|
486
|
+
}
|
487
|
+
}
|
488
|
+
|
489
|
+
/**
|
490
|
+
* consumer_free
|
491
|
+
*
|
492
|
+
* Callback called when Ruby needs to GC the configuration associated with an Hermann instance.
|
493
|
+
*
|
494
|
+
* @param p void* the instance of an HermannInstanceConfig to be freed from allocated memory.
|
495
|
+
*/
|
496
|
+
static void consumer_free(void * p) {
|
497
|
+
|
498
|
+
HermannInstanceConfig* config = (HermannInstanceConfig *)p;
|
499
|
+
|
500
|
+
// the p *should* contain a pointer to the consumerConfig which also must be freed
|
501
|
+
rd_kafka_topic_destroy(config->rkt);
|
502
|
+
|
503
|
+
rd_kafka_destroy(config->rk);
|
504
|
+
|
505
|
+
// clean up the struct
|
506
|
+
free(config);
|
507
|
+
}
|
508
|
+
|
509
|
+
/**
|
510
|
+
* consumer_allocate
|
511
|
+
*
|
512
|
+
* Allocate and wrap an HermannInstanceConfig for this Consumer object.
|
513
|
+
*
|
514
|
+
* @param klass VALUE the class of the enclosing Ruby object.
|
515
|
+
*/
|
516
|
+
static VALUE consumer_allocate(VALUE klass) {
|
517
|
+
|
518
|
+
VALUE obj;
|
519
|
+
|
520
|
+
HermannInstanceConfig* consumerConfig = ALLOC(HermannInstanceConfig);
|
521
|
+
obj = Data_Wrap_Struct(klass, 0, consumer_free, consumerConfig);
|
522
|
+
|
523
|
+
return obj;
|
524
|
+
}
|
525
|
+
|
526
|
+
/**
|
527
|
+
* consumer_initialize
|
528
|
+
*
|
529
|
+
* todo: configure the brokers through passed parameter, later through zk
|
530
|
+
*
|
531
|
+
* Set up the Consumer's HermannInstanceConfig context.
|
532
|
+
*
|
533
|
+
* @param self VALUE the Ruby instance of the Consumer
|
534
|
+
* @param topic VALUE a Ruby string
|
535
|
+
* @param brokers VALUE a Ruby string containing list of host:port
|
536
|
+
* @param partition VALUE a Ruby number
|
537
|
+
*/
|
538
|
+
static VALUE consumer_initialize(VALUE self, VALUE topic, VALUE brokers, VALUE partition) {
|
539
|
+
|
540
|
+
HermannInstanceConfig* consumerConfig;
|
541
|
+
char* topicPtr;
|
542
|
+
char* brokersPtr;
|
543
|
+
int partitionNo;
|
544
|
+
|
545
|
+
topicPtr = StringValuePtr(topic);
|
546
|
+
brokersPtr = StringValuePtr(brokers);
|
547
|
+
partitionNo = FIX2INT(partition);
|
548
|
+
Data_Get_Struct(self, HermannInstanceConfig, consumerConfig);
|
549
|
+
|
550
|
+
consumerConfig->topic = topicPtr;
|
551
|
+
consumerConfig->brokers = brokersPtr;
|
552
|
+
consumerConfig->partition = partitionNo;
|
553
|
+
consumerConfig->run = 1;
|
554
|
+
consumerConfig->exit_eof = 0;
|
555
|
+
consumerConfig->quiet = 0;
|
556
|
+
|
557
|
+
return self;
|
558
|
+
}
|
559
|
+
|
560
|
+
/**
|
561
|
+
* consumer_init_copy
|
562
|
+
*
|
563
|
+
* When copying into a new instance of a Consumer, reproduce the configuration info.
|
564
|
+
*
|
565
|
+
* @param copy VALUE the Ruby Consumer instance (with configuration) as destination
|
566
|
+
* @param orig VALUE the Ruby Consumer instance (with configuration) as source
|
567
|
+
*
|
568
|
+
*/
|
569
|
+
static VALUE consumer_init_copy(VALUE copy, VALUE orig) {
|
570
|
+
HermannInstanceConfig* orig_config;
|
571
|
+
HermannInstanceConfig* copy_config;
|
572
|
+
|
573
|
+
if(copy == orig) {
|
574
|
+
return copy;
|
575
|
+
}
|
576
|
+
|
577
|
+
if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)consumer_free) {
|
578
|
+
rb_raise(rb_eTypeError, "wrong argument type");
|
579
|
+
}
|
580
|
+
|
581
|
+
Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
|
582
|
+
Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
|
583
|
+
|
584
|
+
// Copy over the data from one struct to the other
|
585
|
+
MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
|
586
|
+
|
587
|
+
return copy;
|
588
|
+
}
|
589
|
+
|
590
|
+
/**
|
591
|
+
* producer_free
|
592
|
+
*
|
593
|
+
* Reclaim memory allocated to the Producer's configuration
|
594
|
+
*
|
595
|
+
* @param p void* the instance's configuration struct
|
596
|
+
*/
|
597
|
+
static void producer_free(void * p) {
|
598
|
+
|
599
|
+
HermannInstanceConfig* config = (HermannInstanceConfig *)p;
|
600
|
+
|
601
|
+
// Clean up the topic
|
602
|
+
rd_kafka_topic_destroy(config->rkt);
|
603
|
+
|
604
|
+
// Take care of the producer instance
|
605
|
+
rd_kafka_destroy(config->rk);
|
606
|
+
|
607
|
+
// Free the struct
|
608
|
+
free(config);
|
609
|
+
}
|
610
|
+
|
611
|
+
/**
|
612
|
+
* producer_allocate
|
613
|
+
*
|
614
|
+
* Allocate the memory for a Producer's configuration
|
615
|
+
*
|
616
|
+
* @param klass VALUE the class of the Producer
|
617
|
+
*/
|
618
|
+
static VALUE producer_allocate(VALUE klass) {
|
619
|
+
|
620
|
+
VALUE obj;
|
621
|
+
|
622
|
+
HermannInstanceConfig* producerConfig = ALLOC(HermannInstanceConfig);
|
623
|
+
obj = Data_Wrap_Struct(klass, 0, producer_free, producerConfig);
|
624
|
+
|
625
|
+
return obj;
|
626
|
+
}
|
627
|
+
|
628
|
+
/**
|
629
|
+
* producer_initialize
|
630
|
+
*
|
631
|
+
* Set up the configuration context for the Producer instance
|
632
|
+
*
|
633
|
+
* @param self VALUE the Producer instance
|
634
|
+
* @param topic VALUE the Ruby string naming the topic
|
635
|
+
* @param brokers VALUE a Ruby string containing host:port pairs separated by commas
|
636
|
+
*/
|
637
|
+
static VALUE producer_initialize(VALUE self, VALUE topic, VALUE brokers) {
|
638
|
+
|
639
|
+
HermannInstanceConfig* producerConfig;
|
640
|
+
char* topicPtr;
|
641
|
+
char* brokersPtr;
|
642
|
+
|
643
|
+
topicPtr = StringValuePtr(topic);
|
644
|
+
brokersPtr = StringValuePtr(brokers);
|
645
|
+
Data_Get_Struct(self, HermannInstanceConfig, producerConfig);
|
646
|
+
|
647
|
+
producerConfig->topic = topicPtr;
|
648
|
+
producerConfig->brokers = brokersPtr;
|
649
|
+
/** Using RD_KAFKA_PARTITION_UA specifies we want the partitioner callback to be called to determine the target
|
650
|
+
* partition
|
651
|
+
*/
|
652
|
+
producerConfig->partition = RD_KAFKA_PARTITION_UA;
|
653
|
+
producerConfig->run = 1;
|
654
|
+
producerConfig->exit_eof = 0;
|
655
|
+
producerConfig->quiet = 0;
|
656
|
+
|
657
|
+
return self;
|
658
|
+
}
|
659
|
+
|
660
|
+
/**
|
661
|
+
* producer_init_copy
|
662
|
+
*
|
663
|
+
* Copy the configuration information from orig into copy for the given Producer instances.
|
664
|
+
*
|
665
|
+
* @param copy VALUE destination Producer
|
666
|
+
* @param orign VALUE source Producer
|
667
|
+
*/
|
668
|
+
static VALUE producer_init_copy(VALUE copy, VALUE orig) {
|
669
|
+
HermannInstanceConfig* orig_config;
|
670
|
+
HermannInstanceConfig* copy_config;
|
671
|
+
|
672
|
+
if(copy == orig) {
|
673
|
+
return copy;
|
674
|
+
}
|
675
|
+
|
676
|
+
if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)producer_free) {
|
677
|
+
rb_raise(rb_eTypeError, "wrong argument type");
|
678
|
+
}
|
679
|
+
|
680
|
+
Data_Get_Struct(orig, HermannInstanceConfig, orig_config);
|
681
|
+
Data_Get_Struct(copy, HermannInstanceConfig, copy_config);
|
682
|
+
|
683
|
+
// Copy over the data from one struct to the other
|
684
|
+
MEMCPY(copy_config, orig_config, HermannInstanceConfig, 1);
|
685
|
+
|
686
|
+
return copy;
|
687
|
+
}
|
688
|
+
|
689
|
+
/**
|
690
|
+
* Init_hermann_lib
|
691
|
+
*
|
692
|
+
* Called by Ruby when the Hermann gem is loaded.
|
693
|
+
* Defines the Hermann module.
|
694
|
+
* Defines the Producer and Consumer classes.
|
695
|
+
*/
|
696
|
+
void Init_hermann_lib() {
|
697
|
+
|
698
|
+
/* Define the module */
|
699
|
+
m_hermann = rb_define_module("Hermann");
|
700
|
+
|
701
|
+
/* ---- Define the consumer class ---- */
|
702
|
+
VALUE c_consumer = rb_define_class_under(m_hermann, "Consumer", rb_cObject);
|
703
|
+
|
704
|
+
/* Allocate */
|
705
|
+
rb_define_alloc_func(c_consumer, consumer_allocate);
|
706
|
+
|
707
|
+
/* Initialize */
|
708
|
+
rb_define_method(c_consumer, "initialize", consumer_initialize, 3);
|
709
|
+
rb_define_method(c_consumer, "initialize_copy", consumer_init_copy, 1);
|
710
|
+
|
711
|
+
/* Consumer has method 'consume' */
|
712
|
+
rb_define_method( c_consumer, "consume", consumer_consume, 0 );
|
713
|
+
|
714
|
+
/* ---- Define the producer class ---- */
|
715
|
+
VALUE c_producer = rb_define_class_under(m_hermann, "Producer", rb_cObject);
|
716
|
+
|
717
|
+
/* Allocate */
|
718
|
+
rb_define_alloc_func(c_producer, producer_allocate);
|
719
|
+
|
720
|
+
/* Initialize */
|
721
|
+
rb_define_method(c_producer, "initialize", producer_initialize, 2);
|
722
|
+
rb_define_method(c_producer, "initialize_copy", producer_init_copy, 1);
|
723
|
+
|
724
|
+
/* Producer.push(msg) */
|
725
|
+
rb_define_method( c_producer, "push", producer_push, 1 );
|
726
|
+
|
727
|
+
}
|
data/ext/hermann_lib.h
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
/*
|
2
|
+
* hermann_lib.h - Ruby wrapper for the librdkafka library
|
3
|
+
*
|
4
|
+
* Copyright (c) 2014 Stan Campbell
|
5
|
+
* All rights reserved.
|
6
|
+
*
|
7
|
+
* Redistribution and use in source and binary forms, with or without
|
8
|
+
* modification, are permitted provided that the following conditions are met:
|
9
|
+
*
|
10
|
+
* 1. Redistributions of source code must retain the above copyright notice,
|
11
|
+
* this list of conditions and the following disclaimer.
|
12
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
* this list of conditions and the following disclaimer in the documentation
|
14
|
+
* and/or other materials provided with the distribution.
|
15
|
+
*
|
16
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
17
|
+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
18
|
+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
19
|
+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
20
|
+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
21
|
+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
22
|
+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
23
|
+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
24
|
+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
25
|
+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
26
|
+
* POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
*/
|
28
|
+
|
29
|
+
#ifndef HERMANN_H
|
30
|
+
#define HERMANN_H
|
31
|
+
|
32
|
+
#include <ruby.h>
|
33
|
+
|
34
|
+
#include <ctype.h>
|
35
|
+
#include <signal.h>
|
36
|
+
#include <string.h>
|
37
|
+
#include <unistd.h>
|
38
|
+
#include <stdlib.h>
|
39
|
+
#include <syslog.h>
|
40
|
+
#include <sys/time.h>
|
41
|
+
#include <errno.h>
|
42
|
+
|
43
|
+
#include <librdkafka/rdkafka.h>
|
44
|
+
|
45
|
+
// Holds the defined Ruby module for Hermann
|
46
|
+
static VALUE m_hermann;
|
47
|
+
|
48
|
+
static int DEBUG = 0;
|
49
|
+
|
50
|
+
// Should we expect rb_thread_blocking_region to be present?
|
51
|
+
// #define RB_THREAD_BLOCKING_REGION
|
52
|
+
#undef RB_THREAD_BLOCKING_REGION
|
53
|
+
|
54
|
+
static enum {
|
55
|
+
OUTPUT_HEXDUMP,
|
56
|
+
OUTPUT_RAW,
|
57
|
+
} output = OUTPUT_HEXDUMP;
|
58
|
+
|
59
|
+
typedef struct HermannInstanceConfig {
|
60
|
+
|
61
|
+
char* topic;
|
62
|
+
|
63
|
+
/* Kafka configuration */
|
64
|
+
rd_kafka_t *rk;
|
65
|
+
rd_kafka_topic_t *rkt;
|
66
|
+
char *brokers;
|
67
|
+
int partition;
|
68
|
+
rd_kafka_topic_conf_t *topic_conf;
|
69
|
+
char errstr[512];
|
70
|
+
rd_kafka_conf_t *conf;
|
71
|
+
const char *debug;
|
72
|
+
int64_t start_offset;
|
73
|
+
int do_conf_dump;
|
74
|
+
|
75
|
+
int run;
|
76
|
+
int exit_eof;
|
77
|
+
int quiet;
|
78
|
+
|
79
|
+
int isInitialized;
|
80
|
+
|
81
|
+
} HermannInstanceConfig;
|
82
|
+
|
83
|
+
#endif
|
data/ext/hermann_lib.o
ADDED
Binary file
|
data/lib/hermann.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'hermann_lib'
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hermann
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 11
|
9
|
+
version: "0.11"
|
10
|
+
platform: x86-darwin-12
|
11
|
+
authors:
|
12
|
+
- Stan Campbell
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2014-05-29 00:00:00 Z
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description: Ruby gem wrapper for the RdKafka C library
|
21
|
+
email: stan.campbell3@gmail.com
|
22
|
+
executables: []
|
23
|
+
|
24
|
+
extensions:
|
25
|
+
- ext/extconf.rb
|
26
|
+
extra_rdoc_files: []
|
27
|
+
|
28
|
+
files:
|
29
|
+
- Rakefile
|
30
|
+
- ext/hermann_lib.h
|
31
|
+
- ext/hermann_lib.c
|
32
|
+
- ext/extconf.rb
|
33
|
+
- bin/hermann
|
34
|
+
- lib/hermann.rb
|
35
|
+
- ext/hermann_lib.o
|
36
|
+
- test/test_hermann.rb
|
37
|
+
homepage: http://rubygems.org/gems/hermann
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
- ext
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.25
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: The Kafka consumer is based on the librdkafka C library.
|
71
|
+
test_files:
|
72
|
+
- test/test_hermann.rb
|