hermann 0.18.1-java

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.
@@ -0,0 +1,107 @@
1
+ /*
2
+ * hermann_lib.h - Ruby wrapper for the librdkafka library
3
+ *
4
+ * Copyright (c) 2014 Stan Campbell
5
+ * Copyright (c) 2014 Lookout, Inc.
6
+ * All rights reserved.
7
+ *
8
+ * Redistribution and use in source and binary forms, with or without
9
+ * modification, are permitted provided that the following conditions are met:
10
+ *
11
+ * 1. Redistributions of source code must retain the above copyright notice,
12
+ * this list of conditions and the following disclaimer.
13
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ * this list of conditions and the following disclaimer in the documentation
15
+ * and/or other materials provided with the distribution.
16
+ *
17
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ * POSSIBILITY OF SUCH DAMAGE.
28
+ */
29
+
30
+ #ifndef HERMANN_H
31
+ #define HERMANN_H
32
+
33
+ #include <ruby.h>
34
+
35
+ #include <ctype.h>
36
+ #include <signal.h>
37
+ #include <string.h>
38
+ #include <unistd.h>
39
+ #include <stdlib.h>
40
+ #include <syslog.h>
41
+ #include <sys/time.h>
42
+ #include <errno.h>
43
+
44
+ #include <librdkafka/rdkafka.h>
45
+
46
+ #ifdef TRACE
47
+ #define TRACER(...) do { \
48
+ fprintf(stderr, "%i:%s()> ", __LINE__, __PRETTY_FUNCTION__); \
49
+ fprintf(stderr, __VA_ARGS__); \
50
+ fflush(stderr); \
51
+ } while (0)
52
+ #else
53
+ #define TRACER(...) do { } while (0)
54
+ #endif
55
+
56
+ // Holds the defined Ruby module for Hermann
57
+ static VALUE hermann_module;
58
+
59
+ #define HERMANN_MAX_ERRSTR_LEN 512
60
+
61
+ static int DEBUG = 0;
62
+
63
+ // Should we expect rb_thread_blocking_region to be present?
64
+ // #define RB_THREAD_BLOCKING_REGION
65
+ #undef RB_THREAD_BLOCKING_REGION
66
+
67
+ static enum {
68
+ OUTPUT_HEXDUMP,
69
+ OUTPUT_RAW,
70
+ } output = OUTPUT_HEXDUMP;
71
+
72
+ typedef struct HermannInstanceConfig {
73
+ char *topic;
74
+
75
+ /* Kafka configuration */
76
+ rd_kafka_t *rk;
77
+ rd_kafka_topic_t *rkt;
78
+ char *brokers;
79
+ int partition;
80
+ rd_kafka_topic_conf_t *topic_conf;
81
+ char errstr[512];
82
+ rd_kafka_conf_t *conf;
83
+ const char *debug;
84
+ int64_t start_offset;
85
+ int do_conf_dump;
86
+
87
+ int run;
88
+ int exit_eof;
89
+ int quiet;
90
+
91
+ int isInitialized;
92
+ int isConnected;
93
+
94
+ int isErrored;
95
+ char *error;
96
+ } HermannInstanceConfig;
97
+
98
+ typedef HermannInstanceConfig hermann_conf_t;
99
+
100
+ typedef struct {
101
+ /* Hermann::Lib::Producer */
102
+ hermann_conf_t *producer;
103
+ /* Hermann::Result */
104
+ VALUE result;
105
+ } hermann_push_ctx_t;
106
+
107
+ #endif
@@ -0,0 +1,57 @@
1
+ From 888ca33b571d99e877d665235b822f7c961c8fdb Mon Sep 17 00:00:00 2001
2
+ From: "R. Tyler Croy" <tyler@monkeypox.org>
3
+ Date: Thu, 28 Aug 2014 16:24:04 -0700
4
+ Subject: [PATCH 6/8] Update some headers to include the right headers to build
5
+ on FreeBSD
6
+
7
+ ---
8
+ src/rd.h | 9 +++++++++
9
+ src/rdaddr.h | 4 ++++
10
+ 2 files changed, 13 insertions(+)
11
+
12
+ diff --git a/src/rd.h b/src/rd.h
13
+ index c31501e..4789493 100644
14
+ --- a/src/rd.h
15
+ +++ b/src/rd.h
16
+ @@ -37,7 +37,11 @@
17
+ #include <errno.h>
18
+ #include <time.h>
19
+ #include <sys/time.h>
20
+ +
21
+ +#ifndef __FreeBSD__
22
+ +/* alloca(3) is in stdlib on FreeBSD */
23
+ #include <alloca.h>
24
+ +#endif
25
+ #include <assert.h>
26
+ #include <pthread.h>
27
+
28
+ @@ -110,6 +114,11 @@
29
+ # endif
30
+ #endif /* sun */
31
+
32
+ +#ifdef __FreeBSD__
33
+ +/* FreeBSD defines be64toh() in sys/endian.h */
34
+ +#include <sys/endian.h>
35
+ +#endif
36
+ +
37
+ #ifndef be64toh
38
+ #ifndef __APPLE__
39
+ #ifndef sun
40
+ diff --git a/src/rdaddr.h b/src/rdaddr.h
41
+ index 0b37354..e55bd55 100644
42
+ --- a/src/rdaddr.h
43
+ +++ b/src/rdaddr.h
44
+ @@ -32,6 +32,10 @@
45
+ #include <arpa/inet.h>
46
+ #include <netdb.h>
47
+
48
+ +#ifdef __FreeBSD__
49
+ +#include <sys/socket.h>
50
+ +#endif
51
+ +
52
+ /**
53
+ * rd_sockaddr_inx_t is a union for either ipv4 or ipv6 sockaddrs.
54
+ * It provides conveniant abstraction of AF_INET* agnostic operations.
55
+ --
56
+ 1.9.0
57
+
@@ -0,0 +1,24 @@
1
+ require 'hermann'
2
+
3
+ unless Hermann.jruby?
4
+ require 'hermann_lib'
5
+ end
6
+
7
+ module Hermann
8
+ class Consumer
9
+ attr_reader :topic, :brokers, :partition, :internal
10
+
11
+ def initialize(topic, brokers, partition)
12
+ @topic = topic
13
+ @brokers = brokers
14
+ @partition = partition
15
+ unless Hermann.jruby?
16
+ @internal = Hermann::Lib::Consumer.new(topic, brokers, partition)
17
+ end
18
+ end
19
+
20
+ def consume(&block)
21
+ @internal.consume(&block)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,85 @@
1
+ require 'hermann'
2
+ require 'zk'
3
+ require 'json'
4
+ require 'hermann/errors'
5
+
6
+ module Hermann
7
+ module Discovery
8
+
9
+
10
+ # Communicates with Zookeeper to discover kafka broker ids
11
+ #
12
+ class Zookeeper
13
+ attr_reader :zookeepers
14
+
15
+ BROKERS_PATH = "/brokers/ids".freeze
16
+
17
+ def initialize(zookeepers)
18
+ @zookeepers = zookeepers
19
+ end
20
+
21
+ # Gets comma separated string of brokers
22
+ #
23
+ # @param [Fixnum] timeout to connect to zookeeper, "2 times the
24
+ # tickTime (as set in the server configuration) and a maximum
25
+ # of 20 times the tickTime2 times the tick time set on server"
26
+ #
27
+ # @return [String] comma separated list of brokers
28
+ #
29
+ # @raises [NoBrokersError] if could not discover brokers thru zookeeper
30
+ def get_brokers(timeout=0)
31
+ brokers = []
32
+ ZK.open(zookeepers, {:timeout => timeout}) do |zk|
33
+ brokers = fetch_brokers(zk)
34
+ end
35
+ if brokers.empty?
36
+ raise Hermann::Errors::NoBrokersError
37
+ end
38
+ brokers.join(',')
39
+ end
40
+
41
+ private
42
+
43
+ # Gets an Array of broker strings
44
+ #
45
+ # @param [ZK::Client] zookeeper client
46
+ #
47
+ # @return array of broker strings
48
+ def fetch_brokers(zk)
49
+ brokers = []
50
+ zk.children(BROKERS_PATH).each do |id|
51
+ node = fetch_znode(zk, id)
52
+ next if node.nil? # whatever error could happen from ZK#get
53
+ brokers << format_broker_from_znode(node)
54
+ end
55
+ brokers.compact
56
+ end
57
+
58
+ # Gets node from zookeeper
59
+ #
60
+ # @param [ZK::Client] zookeeper client
61
+ # @param [Fixnum] kafka broker
62
+ #
63
+ # @return [String] node data
64
+ def fetch_znode(zk, id)
65
+ zk.get("#{BROKERS_PATH}/#{id}")[0]
66
+ rescue ZK::Exceptions::NoNode
67
+ nil
68
+ end
69
+
70
+ # Formats the node data into string
71
+ #
72
+ # @param [String] node data
73
+ #
74
+ # @return [String] formatted node data or empty string if error
75
+ def format_broker_from_znode(znode)
76
+ hash = JSON.parse(znode)
77
+ host = hash['host']
78
+ port = hash['port']
79
+ host && port ? "#{host}:#{port}" : nil
80
+ rescue JSON::ParserError
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,14 @@
1
+
2
+ module Hermann
3
+ module Errors
4
+ # Error for connectivity problems with the Kafka brokers
5
+ class ConnectivityError; end;
6
+
7
+ # For passing incorrect config and options to kafka
8
+ class ConfigurationError < StandardError; end
9
+
10
+ # cannot discover brokers from zookeeper
11
+ class NoBrokersError < StandardError; end
12
+ end
13
+ end
14
+
@@ -0,0 +1,150 @@
1
+ require 'hermann'
2
+ require 'hermann/result'
3
+
4
+
5
+ if RUBY_PLATFORM == "java"
6
+ require 'hermann/provider/java_producer'
7
+ else
8
+ require 'hermann_lib'
9
+ end
10
+
11
+ module Hermann
12
+ class Producer
13
+ attr_reader :topic, :brokers, :internal, :children
14
+
15
+ # Initialize a producer object with a default topic and broker list
16
+ #
17
+ # @param [String] topic The default topic to use for pushing messages
18
+ # @param [Array] brokers An array of "host:port" strings for the brokers
19
+ def initialize(topic, brokers, opts={})
20
+ @topic = topic
21
+ @brokers = brokers
22
+ if RUBY_PLATFORM == "java"
23
+ @internal = Hermann::Provider::JavaProducer.new(brokers, opts)
24
+ else
25
+ @internal = Hermann::Lib::Producer.new(brokers)
26
+ end
27
+ # We're tracking children so we can make sure that at Producer exit we
28
+ # make a reasonable attempt to clean up outstanding result objects
29
+ @children = []
30
+ end
31
+
32
+ # @return [Boolean] True if our underlying producer object thinks it's
33
+ # connected to a Kafka broker
34
+ def connected?
35
+ return @internal.connected?
36
+ end
37
+
38
+ # @return [Boolean] True if the underlying producer object has errored
39
+ def errored?
40
+ return @internal.errored?
41
+ end
42
+
43
+ def connect(timeout=0)
44
+ return @internal.connect(timeout * 1000)
45
+ end
46
+
47
+ # Push a value onto the Kafka topic passed to this +Producer+
48
+ #
49
+ # @param [Object] value A single object to push
50
+ # @param [Hash] opts to pass to push method
51
+ # @option opts [String] :topic The topic to push messages to
52
+ #
53
+ # @return [Hermann::Result] A future-like object which will store the
54
+ # result from the broker
55
+ def push(value, opts={})
56
+ topic = opts[:topic] || @topic
57
+ result = nil
58
+
59
+ if value.kind_of? Array
60
+ return value.map { |e| self.push(e, opts) }
61
+ end
62
+
63
+ if RUBY_PLATFORM == "java"
64
+ result = @internal.push_single(value, topic, nil)
65
+ unless result.nil?
66
+ @children << result
67
+ end
68
+ # Reaping children on the push just to make sure that it does get
69
+ # called correctly and we don't leak memory
70
+ reap_children
71
+ else
72
+ result = create_result
73
+ @internal.push_single(value, topic, result)
74
+ end
75
+
76
+ return result
77
+ end
78
+
79
+ # Create a +Hermann::Result+ that is tracked in the Producer's children
80
+ # array
81
+ #
82
+ # @return [Hermann::Result] A new, unused, result
83
+ def create_result
84
+ @children << Hermann::Result.new(self)
85
+ return @children.last
86
+ end
87
+
88
+ # Tick the underlying librdkafka reacter and clean up any unreaped but
89
+ # reapable children results
90
+ #
91
+ # @param [FixNum] timeout Seconds to block on the internal reactor
92
+ # @return [FixNum] Number of +Hermann::Result+ children reaped
93
+ def tick_reactor(timeout=0)
94
+ begin
95
+ execute_tick(rounded_timeout(timeout))
96
+ rescue StandardError => ex
97
+ @children.each do |child|
98
+ # Skip over any children that should already be reaped for other
99
+ # reasons
100
+ next if child.completed?
101
+ # Propagate errors to the remaining children
102
+ child.internal_set_error(ex)
103
+ end
104
+ end
105
+
106
+ # Reaping the children at this point will also reap any children marked
107
+ # as errored by an exception out of #execute_tick
108
+ return reap_children
109
+ end
110
+
111
+ # @return [FixNum] number of children reaped
112
+ def reap_children
113
+ # Filter all children who are no longer pending/fulfilled
114
+ total_children = @children.size
115
+
116
+ @children = @children.reject { |c| c.completed? }
117
+
118
+ return (total_children - children.size)
119
+ end
120
+
121
+
122
+ private
123
+
124
+ def rounded_timeout(timeout)
125
+ # Handle negative numbers, those can be zero
126
+ return 0 if (timeout < 0)
127
+ # Since we're going to sleep for each second, round any potential floats
128
+ # off
129
+ return timeout.round if timeout.kind_of?(Float)
130
+ return timeout
131
+ end
132
+
133
+ # Perform the actual reactor tick
134
+ # @raises [StandardError] in case of underlying failures in librdkafka
135
+ def execute_tick(timeout)
136
+ if timeout == 0
137
+ @internal.tick(0)
138
+ else
139
+ (timeout * 2).times do
140
+ # We're going to Thread#sleep in Ruby to avoid a
141
+ # pthread_cond_timedwait(3) inside of librdkafka
142
+ events = @internal.tick(0)
143
+ # If we find events, break out early
144
+ break if events > 0
145
+ sleep 0.5
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,106 @@
1
+ require 'hermann'
2
+ require 'concurrent'
3
+ require 'json'
4
+
5
+ module Hermann
6
+ module Provider
7
+ # This class simulates the kafka producer class within a java environment.
8
+ # If the producer throw an exception within the Promise a call to +.value!+
9
+ # will raise the exception and the rejected flag will be set to true
10
+ #
11
+ class JavaProducer
12
+ attr_accessor :producer
13
+
14
+
15
+ # Instantiate JavaProducer
16
+ #
17
+ # @params [String] list of brokers
18
+ #
19
+ # @params [Hash] hash of kafka attributes, overrides defaults
20
+ #
21
+ # @raises [RuntimeError] if brokers string is nil/empty
22
+ #
23
+ # ==== Examples
24
+ #
25
+ # JavaProducer.new('0:9092', {'request.required.acks' => '1'})
26
+ #
27
+ def initialize(brokers, opts={})
28
+ properties = create_properties(brokers, opts)
29
+ config = create_config(properties)
30
+ @producer = JavaApiUtil::Producer.new(config)
31
+ end
32
+
33
+ DEFAULTS = {
34
+ 'serializer.class' => 'kafka.serializer.StringEncoder',
35
+ 'partitioner.class' => 'kafka.producer.DefaultPartitioner',
36
+ 'request.required.acks' => '1'
37
+ }.freeze
38
+
39
+ # Push a value onto the Kafka topic passed to this +Producer+
40
+ #
41
+ # @param [Object] value A single object to push
42
+ # @param [String] topic to push message to
43
+ #
44
+ # @return +Concurrent::Promise+ Representa a promise to send the
45
+ # data to the kafka broker. Upon execution the Promise's status
46
+ # will be set
47
+ def push_single(msg, topic, unused)
48
+ Concurrent::Promise.execute {
49
+ data = ProducerUtil::KeyedMessage.new(topic, msg)
50
+ @producer.send(data)
51
+ }
52
+ end
53
+
54
+ # No-op for now
55
+ def connected?
56
+ return false
57
+ end
58
+
59
+ # No-op for now
60
+ def errored?
61
+ return false
62
+ end
63
+
64
+ # No-op for now
65
+ def connect(timeout=0)
66
+ nil
67
+ end
68
+
69
+ private
70
+
71
+ # Creates a ProducerConfig object
72
+ #
73
+ # @param [Properties] object with broker properties
74
+ #
75
+ # @return [ProducerConfig] - packaged config for +Producer+
76
+ def create_config(properties)
77
+ ProducerUtil::ProducerConfig.new(properties)
78
+ end
79
+
80
+ # Creates Properties Object
81
+ #
82
+ # @param [Hash] brokers passed into this function
83
+ # @option args [String] :brokers - string of brokers
84
+ #
85
+ # @return [Properties] properties object for creating +ProducerConfig+
86
+ #
87
+ # @raises [RuntimeError] if options does not contain key value strings
88
+ def create_properties(brokers, opts={})
89
+ brokers = { 'metadata.broker.list' => brokers }
90
+ options = DEFAULTS.merge(brokers).merge(opts)
91
+ properties = JavaUtil::Properties.new
92
+ options.each do |key, val|
93
+ validate_property!(key, val)
94
+ properties.put(key, val)
95
+ end
96
+ properties
97
+ end
98
+
99
+ def validate_property!(key, val)
100
+ if key.to_s.empty? || val.to_s.empty?
101
+ raise Hermann::Errors::ConfigurationError, "Invalid Broker Properties"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,74 @@
1
+
2
+ module Hermann
3
+ class Result
4
+ attr_reader :reason, :state
5
+
6
+ STATES = [:pending,
7
+ :rejected,
8
+ :fulfilled,
9
+ :unfulfilled,
10
+ ].freeze
11
+
12
+ def initialize(producer)
13
+ @producer = producer
14
+ @reason = nil
15
+ @value = nil
16
+ @state = :unfulfilled
17
+ end
18
+
19
+ STATES.each do |state|
20
+ define_method("#{state}?".to_sym) do
21
+ return @state == state
22
+ end
23
+ end
24
+
25
+ # @return [Boolean] True if this child can be reaped
26
+ def completed?
27
+ return true if rejected? || fulfilled?
28
+ return false
29
+ end
30
+
31
+ # Access the value of the future
32
+ #
33
+ # @param [FixNum] timeout Seconds to wait on the underlying machinery for a
34
+ # result
35
+ # @return [NilClass] nil if no value could be received in the time alotted
36
+ # @return [Object]
37
+ def value(timeout=0)
38
+ @producer.tick_reactor(timeout)
39
+ return @value
40
+ end
41
+
42
+ # INTERNAL METHOD ONLY. Do not use
43
+ #
44
+ # This method will be invoked by the underlying extension to indicate set
45
+ # the actual value after a callback has completed
46
+ #
47
+ # @param [Object] value The actual resulting value
48
+ # @param [Boolean] is_error True if the result was errored for whatever
49
+ # reason
50
+ def internal_set_value(value, is_error)
51
+ @value = value
52
+
53
+ if is_error
54
+ puts "Hermann::Result#set_internal_value(#{value.class}:\"#{value}\", error?:#{is_error})"
55
+ @state = :rejected
56
+ else
57
+ @state = :fulfilled
58
+ end
59
+ end
60
+
61
+ # INTERNAL METHOD ONLY. Do not use
62
+ #
63
+ # This method will set our internal #reason with the details from the
64
+ # exception
65
+ #
66
+ # @param [Exception] exception
67
+ def internal_set_error(exception)
68
+ return if exception.nil?
69
+
70
+ @reason = exception
71
+ @state = :rejected
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'system_timer'
3
+ module Hermann
4
+ class Timeout
5
+ USE_SYSTEM_TIMER = true
6
+ end
7
+ end
8
+ rescue LoadError
9
+ require 'timeout'
10
+
11
+ if RUBY_VERSION == '1.8.7'
12
+ puts ">>> You are running on 1.8.7 without SystemTimer"
13
+ puts ">>> which means Hermann::Timeout will not work as expected"
14
+ end
15
+ module Hermann
16
+ class Timeout
17
+ USE_SYSTEM_TIMER = false
18
+ end
19
+ end
20
+ end
21
+
22
+ module Hermann
23
+ class Timeout
24
+ def self.system_timer?
25
+ Hermann::Timeout::USE_SYSTEM_TIMER
26
+ end
27
+
28
+ def self.timeout(seconds, klass=nil, &block)
29
+ if system_timer?
30
+ SystemTimer.timeout_after(seconds, klass, &block)
31
+ else
32
+ ::Timeout.timeout(seconds, klass, &block)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,3 @@
1
+ module Hermann
2
+ VERSION = '0.18.1'
3
+ end
data/lib/hermann.rb ADDED
@@ -0,0 +1,20 @@
1
+ module Hermann
2
+ def self.jruby?
3
+ return RUBY_PLATFORM == "java"
4
+ end
5
+
6
+ if self.jruby?
7
+ require 'java'
8
+ require 'hermann_jars'
9
+
10
+ module JavaUtil
11
+ include_package 'java.util'
12
+ end
13
+ module ProducerUtil
14
+ include_package 'kafka.producer'
15
+ end
16
+ module JavaApiUtil
17
+ include_package 'kafka.javaapi.producer'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # this is a generated file, to avoid over-writing it just delete this comment
2
+ require 'jar_dependencies'
3
+
4
+ require_jar( 'org.slf4j', 'slf4j-api', '1.7.2' )
5
+ require_jar( 'org.scala-lang', 'scala-library', '2.10.1' )
6
+ require_jar( 'log4j', 'log4j', '1.2.14' )
7
+ require_jar( 'com.yammer.metrics', 'metrics-core', '2.2.0' )
8
+ require_jar( 'org.apache.zookeeper', 'zookeeper', '3.3.4' )
9
+ require_jar( 'net.sf.jopt-simple', 'jopt-simple', '3.2' )
10
+ require_jar( 'org.apache.kafka', 'kafka_2.10', '0.8.1.1' )
11
+ require_jar( 'jline', 'jline', '0.9.94' )
12
+ require_jar( 'com.101tec', 'zkclient', '0.3' )
13
+ require_jar( 'org.mod4j.org.eclipse.xtext', 'log4j', '1.2.15' )
14
+ require_jar( 'junit', 'junit', '3.8.1' )
15
+ require_jar( 'org.xerial.snappy', 'snappy-java', '1.0.5' )