fluentd 0.14.9 → 0.14.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/ChangeLog +44 -0
  4. data/appveyor.yml +1 -0
  5. data/code-of-conduct.md +3 -0
  6. data/fluentd.gemspec +1 -1
  7. data/lib/fluent/command/cat.rb +11 -3
  8. data/lib/fluent/compat/output.rb +6 -3
  9. data/lib/fluent/compat/parser.rb +2 -0
  10. data/lib/fluent/config/section.rb +1 -1
  11. data/lib/fluent/env.rb +1 -1
  12. data/lib/fluent/plugin/filter_record_transformer.rb +12 -30
  13. data/lib/fluent/plugin/in_forward.rb +50 -169
  14. data/lib/fluent/plugin/in_monitor_agent.rb +8 -4
  15. data/lib/fluent/plugin/in_syslog.rb +13 -7
  16. data/lib/fluent/plugin/in_tail.rb +29 -14
  17. data/lib/fluent/plugin/in_tcp.rb +54 -14
  18. data/lib/fluent/plugin/in_udp.rb +49 -13
  19. data/lib/fluent/plugin/out_file.rb +30 -14
  20. data/lib/fluent/plugin/out_forward.rb +199 -173
  21. data/lib/fluent/plugin/output.rb +71 -46
  22. data/lib/fluent/plugin/parser_json.rb +1 -1
  23. data/lib/fluent/plugin_helper.rb +2 -0
  24. data/lib/fluent/plugin_helper/event_loop.rb +24 -6
  25. data/lib/fluent/plugin_helper/inject.rb +12 -1
  26. data/lib/fluent/plugin_helper/server.rb +494 -0
  27. data/lib/fluent/plugin_helper/socket.rb +101 -0
  28. data/lib/fluent/plugin_helper/socket_option.rb +84 -0
  29. data/lib/fluent/plugin_helper/timer.rb +1 -0
  30. data/lib/fluent/test/driver/base.rb +45 -13
  31. data/lib/fluent/version.rb +1 -1
  32. data/lib/fluent/winsvc.rb +1 -1
  33. data/test/compat/test_parser.rb +10 -0
  34. data/test/config/test_configurable.rb +20 -0
  35. data/test/helper.rb +36 -1
  36. data/test/plugin/test_filter_record_transformer.rb +31 -103
  37. data/test/plugin/test_in_forward.rb +13 -75
  38. data/test/plugin/test_in_monitor_agent.rb +65 -35
  39. data/test/plugin/test_in_syslog.rb +39 -3
  40. data/test/plugin/test_in_tcp.rb +78 -62
  41. data/test/plugin/test_in_udp.rb +101 -80
  42. data/test/plugin/test_out_file.rb +17 -0
  43. data/test/plugin/test_out_forward.rb +155 -125
  44. data/test/plugin/test_output_as_buffered.rb +4 -2
  45. data/test/plugin_helper/test_inject.rb +21 -0
  46. data/test/plugin_helper/test_server.rb +905 -0
  47. data/test/test_event_time.rb +3 -1
  48. data/test/test_output.rb +30 -1
  49. data/test/test_test_drivers.rb +5 -2
  50. metadata +19 -6
@@ -40,6 +40,8 @@ module Fluent
40
40
 
41
41
  CHUNKING_FIELD_WARN_NUM = 4
42
42
 
43
+ PROCESS_CLOCK_ID = Process::CLOCK_MONOTONIC_RAW rescue Process::CLOCK_MONOTONIC
44
+
43
45
  config_param :time_as_integer, :bool, default: false
44
46
 
45
47
  # `<buffer>` and `<secondary>` sections are available only when '#format' and '#write' are implemented
@@ -138,7 +140,7 @@ module Fluent
138
140
  end
139
141
 
140
142
  # Internal states
141
- FlushThreadState = Struct.new(:thread, :next_time)
143
+ FlushThreadState = Struct.new(:thread, :next_clock)
142
144
  DequeuedChunkInfo = Struct.new(:chunk_id, :time, :timeout) do
143
145
  def expired?
144
146
  time + timeout < Time.now
@@ -898,9 +900,9 @@ module Fluent
898
900
  @retry_mutex.synchronize do
899
901
  if @retry # success to flush chunks in retries
900
902
  if secondary
901
- log.warn "retry succeeded by secondary.", plugin_id: plugin_id, chunk_id: dump_unique_id_hex(chunk_id)
903
+ log.warn "retry succeeded by secondary.", chunk_id: dump_unique_id_hex(chunk_id)
902
904
  else
903
- log.warn "retry succeeded.", plugin_id: plugin_id, chunk_id: dump_unique_id_hex(chunk_id)
905
+ log.warn "retry succeeded.", chunk_id: dump_unique_id_hex(chunk_id)
904
906
  end
905
907
  @retry = nil
906
908
  end
@@ -918,6 +920,8 @@ module Fluent
918
920
  # in many cases, false can be just ignored
919
921
  if @buffer.takeback_chunk(chunk_id)
920
922
  @counters_monitor.synchronize{ @rollback_count += 1 }
923
+ primary = @as_secondary ? @primary_instance : self
924
+ primary.update_retry_state(chunk_id, @as_secondary)
921
925
  true
922
926
  else
923
927
  false
@@ -930,7 +934,9 @@ module Fluent
930
934
  info = @dequeued_chunks.shift
931
935
  if @buffer.takeback_chunk(info.chunk_id)
932
936
  @counters_monitor.synchronize{ @rollback_count += 1 }
933
- log.warn "failed to flush the buffer chunk, timeout to commit.", plugin_id: plugin_id, chunk_id: dump_unique_id_hex(info.chunk_id), flushed_at: info.time
937
+ log.warn "failed to flush the buffer chunk, timeout to commit.", chunk_id: dump_unique_id_hex(info.chunk_id), flushed_at: info.time
938
+ primary = @as_secondary ? @primary_instance : self
939
+ primary.update_retry_state(info.chunk_id, @as_secondary)
934
940
  end
935
941
  end
936
942
  end
@@ -943,7 +949,9 @@ module Fluent
943
949
  info = @dequeued_chunks.shift
944
950
  if @buffer.takeback_chunk(info.chunk_id)
945
951
  @counters_monitor.synchronize{ @rollback_count += 1 }
946
- log.info "delayed commit for buffer chunks was cancelled in shutdown", plugin_id: plugin_id, chunk_id: dump_unique_id_hex(info.chunk_id)
952
+ log.info "delayed commit for buffer chunks was cancelled in shutdown", chunk_id: dump_unique_id_hex(info.chunk_id)
953
+ primary = @as_secondary ? @primary_instance : self
954
+ primary.update_retry_state(info.chunk_id, @as_secondary)
947
955
  end
948
956
  end
949
957
  end
@@ -997,7 +1005,7 @@ module Fluent
997
1005
  log.trace "done to commit a chunk", chunk: dump_chunk_id
998
1006
  end
999
1007
  rescue => e
1000
- log.debug "taking back chunk for errors.", plugin_id: plugin_id, chunk: dump_unique_id_hex(chunk.unique_id)
1008
+ log.debug "taking back chunk for errors.", chunk: dump_unique_id_hex(chunk.unique_id)
1001
1009
  if output.delayed_commit
1002
1010
  @dequeued_chunks_mutex.synchronize do
1003
1011
  @dequeued_chunks.delete_if{|d| d.chunk_id == chunk.unique_id }
@@ -1005,35 +1013,52 @@ module Fluent
1005
1013
  end
1006
1014
  @buffer.takeback_chunk(chunk.unique_id)
1007
1015
 
1008
- @retry_mutex.synchronize do
1009
- if @retry
1010
- @counters_monitor.synchronize{ @num_errors += 1 }
1011
- if @retry.limit?
1012
- records = @buffer.queued_records
1013
- log.error "failed to flush the buffer, and hit limit for retries. dropping all chunks in the buffer queue.", plugin_id: plugin_id, retry_times: @retry.steps, records: records, error: e
1014
- log.error_backtrace e.backtrace
1015
- @buffer.clear_queue!
1016
- log.debug "buffer queue cleared", plugin_id: plugin_id
1017
- @retry = nil
1018
- else
1019
- @retry.step
1020
- msg = if using_secondary
1021
- "failed to flush the buffer with secondary output."
1022
- else
1023
- "failed to flush the buffer."
1024
- end
1025
- log.warn msg, plugin_id: plugin_id, retry_time: @retry.steps, next_retry: @retry.next_time, chunk: dump_unique_id_hex(chunk.unique_id), error: e
1026
- log.warn_backtrace e.backtrace
1027
- end
1016
+ update_retry_state(chunk.unique_id, using_secondary, e)
1017
+
1018
+ raise if @under_plugin_development && !@retry_for_error_chunk
1019
+ end
1020
+ end
1021
+
1022
+ def update_retry_state(chunk_id, using_secondary, error = nil)
1023
+ @retry_mutex.synchronize do
1024
+ @counters_monitor.synchronize{ @num_errors += 1 }
1025
+ chunk_id_hex = dump_unique_id_hex(chunk_id)
1026
+
1027
+ unless @retry
1028
+ @retry = retry_state(@buffer_config.retry_randomize)
1029
+ if error
1030
+ log.warn "failed to flush the buffer.", retry_time: @retry.steps, next_retry_seconds: @retry.next_time, chunk: chunk_id_hex, error: error
1031
+ log.warn_backtrace error.backtrace
1032
+ end
1033
+ return
1034
+ end
1035
+
1036
+ # @retry exists
1037
+
1038
+ if error
1039
+ if @retry.limit?
1040
+ records = @buffer.queued_records
1041
+ msg = "failed to flush the buffer, and hit limit for retries. dropping all chunks in the buffer queue."
1042
+ log.error msg, retry_times: @retry.steps, records: records, error: error
1043
+ log.error_backtrace error.backtrace
1044
+ elsif using_secondary
1045
+ msg = "failed to flush the buffer with secondary output."
1046
+ log.warn msg, retry_time: @retry.steps, next_retry_seconds: @retry.next_time, chunk: chunk_id_hex, error: error
1047
+ log.warn_backtrace error.backtrace
1028
1048
  else
1029
- @retry = retry_state(@buffer_config.retry_randomize)
1030
- @counters_monitor.synchronize{ @num_errors += 1 }
1031
- log.warn "failed to flush the buffer.", plugin_id: plugin_id, retry_time: @retry.steps, next_retry: @retry.next_time, chunk: dump_unique_id_hex(chunk.unique_id), error: e
1032
- log.warn_backtrace e.backtrace
1049
+ msg = "failed to flush the buffer."
1050
+ log.warn msg, retry_time: @retry.steps, next_retry_seconds: @retry.next_time, chunk: chunk_id_hex, error: error
1051
+ log.warn_backtrace error.backtrace
1033
1052
  end
1034
1053
  end
1035
1054
 
1036
- raise if @under_plugin_development && !@retry_for_error_chunk
1055
+ if @retry.limit?
1056
+ @buffer.clear_queue!
1057
+ log.debug "buffer queue cleared"
1058
+ @retry = nil
1059
+ else
1060
+ @retry.step
1061
+ end
1037
1062
  end
1038
1063
  end
1039
1064
 
@@ -1060,7 +1085,7 @@ module Fluent
1060
1085
  # Without locks: it is rough but enough to select "next" writer selection
1061
1086
  @output_flush_thread_current_position = (@output_flush_thread_current_position + 1) % @buffer_config.flush_thread_count
1062
1087
  state = @output_flush_threads[@output_flush_thread_current_position]
1063
- state.next_time = 0
1088
+ state.next_clock = 0
1064
1089
  if state.thread && state.thread.status # "run"/"sleep"/"aborting" or false(successfully stop) or nil(killed by exception)
1065
1090
  state.thread.run
1066
1091
  else
@@ -1102,7 +1127,7 @@ module Fluent
1102
1127
  # only for tests of output plugin
1103
1128
  def flush_thread_wakeup
1104
1129
  @output_flush_threads.each do |state|
1105
- state.next_time = 0
1130
+ state.next_clock = 0
1106
1131
  state.thread.run
1107
1132
  end
1108
1133
  end
@@ -1156,7 +1181,7 @@ module Fluent
1156
1181
  end
1157
1182
  rescue => e
1158
1183
  raise if @under_plugin_development
1159
- log.error "unexpected error while checking flushed chunks. ignored.", plugin_id: plugin_id, error: e
1184
+ log.error "unexpected error while checking flushed chunks. ignored.", error: e
1160
1185
  log.error_backtrace
1161
1186
  ensure
1162
1187
  @output_enqueue_thread_waiting = false
@@ -1166,7 +1191,7 @@ module Fluent
1166
1191
  end
1167
1192
  rescue => e
1168
1193
  # normal errors are rescued by inner begin-rescue clause.
1169
- log.error "error on enqueue thread", plugin_id: plugin_id, error: e
1194
+ log.error "error on enqueue thread", error: e
1170
1195
  log.error_backtrace
1171
1196
  raise
1172
1197
  end
@@ -1175,9 +1200,7 @@ module Fluent
1175
1200
  def flush_thread_run(state)
1176
1201
  flush_thread_interval = @buffer_config.flush_thread_interval
1177
1202
 
1178
- # If the given clock_id is not supported, Errno::EINVAL is raised.
1179
- clock_id = Process::CLOCK_MONOTONIC rescue Process::CLOCK_MONOTONIC_RAW
1180
- state.next_time = Process.clock_gettime(clock_id) + flush_thread_interval
1203
+ state.next_clock = Process.clock_gettime(PROCESS_CLOCK_ID) + flush_thread_interval
1181
1204
 
1182
1205
  while !self.after_started? && !self.stopped?
1183
1206
  sleep 0.5
@@ -1187,16 +1210,18 @@ module Fluent
1187
1210
  begin
1188
1211
  # This thread don't use `thread_current_running?` because this thread should run in `before_shutdown` phase
1189
1212
  while @output_flush_threads_running
1190
- time = Process.clock_gettime(clock_id)
1191
- interval = state.next_time - time
1213
+ current_clock = Process.clock_gettime(PROCESS_CLOCK_ID)
1214
+ interval = state.next_clock - current_clock
1192
1215
 
1193
- if state.next_time <= time
1216
+ if state.next_clock <= current_clock && (!@retry || @retry_mutex.synchronize{ @retry.next_time } <= Time.now)
1194
1217
  try_flush
1195
- # next_flush_interval uses flush_thread_interval or flush_thread_burst_interval (or retrying)
1218
+
1219
+ # next_flush_time uses flush_thread_interval or flush_thread_burst_interval (or retrying)
1196
1220
  interval = next_flush_time.to_f - Time.now.to_f
1197
- # TODO: if secondary && delayed-commit, next_flush_time will be much longer than expected (because @retry still exists)
1198
- # @retry should be cleared if delayed commit is enabled? Or any other solution?
1199
- state.next_time = Process.clock_gettime(clock_id) + interval
1221
+ # TODO: if secondary && delayed-commit, next_flush_time will be much longer than expected
1222
+ # because @retry still exists (#commit_write is not called yet in #try_flush)
1223
+ # @retry should be cleared if delayed commit is enabled? Or any other solution?
1224
+ state.next_clock = Process.clock_gettime(PROCESS_CLOCK_ID) + interval
1200
1225
  end
1201
1226
 
1202
1227
  if @dequeued_chunks_mutex.synchronize{ !@dequeued_chunks.empty? && @dequeued_chunks.first.expired? }
@@ -1210,7 +1235,7 @@ module Fluent
1210
1235
  rescue => e
1211
1236
  # normal errors are rescued by output plugins in #try_flush
1212
1237
  # so this rescue section is for critical & unrecoverable errors
1213
- log.error "error on output thread", plugin_id: plugin_id, error: e
1238
+ log.error "error on output thread", error: e
1214
1239
  log.error_backtrace
1215
1240
  raise
1216
1241
  end
@@ -53,7 +53,7 @@ module Fluent
53
53
  end
54
54
  rescue LoadError
55
55
  name = :yajl
56
- log.info "Oj is not installed, and failing back to Yajl for json parser"
56
+ log.info "Oj is not installed, and failing back to Yajl for json parser" if log
57
57
  retry
58
58
  end
59
59
 
@@ -24,6 +24,8 @@ require 'fluent/plugin_helper/parser'
24
24
  require 'fluent/plugin_helper/formatter'
25
25
  require 'fluent/plugin_helper/inject'
26
26
  require 'fluent/plugin_helper/extract'
27
+ require 'fluent/plugin_helper/socket'
28
+ require 'fluent/plugin_helper/server'
27
29
  require 'fluent/plugin_helper/retry_state'
28
30
  require 'fluent/plugin_helper/compat_parameters'
29
31
 
@@ -30,12 +30,16 @@ module Fluent
30
30
  # terminate: initialize internal state
31
31
 
32
32
  EVENT_LOOP_RUN_DEFAULT_TIMEOUT = 0.5
33
+ EVENT_LOOP_SHUTDOWN_TIMEOUT = 5
34
+ EVENT_LOOP_CLOCK_ID = Process::CLOCK_MONOTONIC_RAW rescue Process::CLOCK_MONOTONIC
33
35
 
34
36
  attr_reader :_event_loop # for tests
35
37
 
36
38
  def event_loop_attach(watcher)
37
39
  @_event_loop_mutex.synchronize do
38
40
  @_event_loop.attach(watcher)
41
+ @_event_loop_attached_watchers << watcher
42
+ watcher
39
43
  end
40
44
  end
41
45
 
@@ -58,6 +62,7 @@ module Fluent
58
62
  @_event_loop_mutex = Mutex.new
59
63
  # plugin MAY configure loop run timeout in #configure
60
64
  @_event_loop_run_timeout = EVENT_LOOP_RUN_DEFAULT_TIMEOUT
65
+ @_event_loop_attached_watchers = []
61
66
  end
62
67
 
63
68
  def start
@@ -65,19 +70,32 @@ module Fluent
65
70
 
66
71
  # event loop does not run here, so mutex lock is not required
67
72
  thread_create :event_loop do
68
- default_watcher = DefaultWatcher.new
69
- @_event_loop.attach(default_watcher)
70
- @_event_loop_running = true
71
- @_event_loop.run(@_event_loop_run_timeout) # this method blocks
72
- @_event_loop_running = false
73
+ begin
74
+ default_watcher = DefaultWatcher.new
75
+ event_loop_attach(default_watcher)
76
+ @_event_loop_running = true
77
+ @_event_loop.run(@_event_loop_run_timeout) # this method blocks
78
+ ensure
79
+ @_event_loop_running = false
80
+ end
73
81
  end
74
82
  end
75
83
 
76
84
  def shutdown
77
85
  @_event_loop_mutex.synchronize do
78
- @_event_loop.watchers.each {|w| w.detach if w.attached? }
86
+ @_event_loop_attached_watchers.reverse.each do |w|
87
+ if w.attached?
88
+ w.detach
89
+ end
90
+ end
79
91
  end
92
+ timeout_at = Process.clock_gettime(EVENT_LOOP_CLOCK_ID) + EVENT_LOOP_SHUTDOWN_TIMEOUT
80
93
  while @_event_loop_running
94
+ if Process.clock_gettime(EVENT_LOOP_CLOCK_ID) >= timeout_at
95
+ log.warn "event loop does NOT exit until hard timeout."
96
+ raise "event loop does NOT exit until hard timeout." if @under_plugin_development
97
+ break
98
+ end
81
99
  sleep 0.1
82
100
  end
83
101
 
@@ -97,9 +97,20 @@ module Fluent
97
97
  if @inject_config
98
98
  @_inject_hostname_key = @inject_config.hostname_key
99
99
  if @_inject_hostname_key
100
+ if self.respond_to?(:buffer_config)
101
+ # Output plugin cannot use "hostname"(specified by @hostname_key),
102
+ # injected by this plugin helper, in chunk keys.
103
+ # This plugin helper works in `#format` (in many cases), but modified record
104
+ # don't have any side effect in chunking of output plugin.
105
+ if self.buffer_config.chunk_keys.include?(@_inject_hostname_key)
106
+ log.error "Use filters to inject hostname to use it in buffer chunking."
107
+ raise Fluent::ConfigError, "the key specified by 'hostname_key' in <inject> cannot be used in buffering chunk key."
108
+ end
109
+ end
110
+
100
111
  @_inject_hostname = @inject_config.hostname
101
112
  unless @_inject_hostname
102
- @_inject_hostname = Socket.gethostname
113
+ @_inject_hostname = ::Socket.gethostname
103
114
  log.info "using hostname for specified field", host_key: @_inject_hostname_key, host_name: @_inject_hostname
104
115
  end
105
116
  end
@@ -0,0 +1,494 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'fluent/plugin_helper/event_loop'
18
+
19
+ require 'serverengine'
20
+ require 'cool.io'
21
+ require 'socket'
22
+ require 'ipaddr'
23
+ require 'fcntl'
24
+
25
+ require_relative 'socket_option'
26
+
27
+ module Fluent
28
+ module PluginHelper
29
+ module Server
30
+ include Fluent::PluginHelper::EventLoop
31
+ include Fluent::PluginHelper::SocketOption
32
+
33
+ # This plugin helper doesn't support these things for now:
34
+ # * SSL/TLS (TBD)
35
+ # * TCP/TLS keepalive
36
+
37
+ # stop : [-]
38
+ # shutdown : detach server event handler from event loop (event_loop)
39
+ # close : close listening sockets
40
+ # terminate: remote all server instances
41
+
42
+ attr_reader :_servers # for tests
43
+
44
+ def server_wait_until_start
45
+ # event_loop_wait_until_start works well for this
46
+ end
47
+
48
+ def server_wait_until_stop
49
+ sleep 0.1 while @_servers.any?{|si| si.server.attached? }
50
+ @_servers.each{|si| si.server.close rescue nil }
51
+ end
52
+
53
+ PROTOCOLS = [:tcp, :udp, :tls, :unix]
54
+ CONNECTION_PROTOCOLS = [:tcp, :tls, :unix]
55
+
56
+ # server_create_connection(:title, @port) do |conn|
57
+ # # on connection
58
+ # source_addr = conn.remote_host
59
+ # source_port = conn.remote_port
60
+ # conn.data do |data|
61
+ # # on data
62
+ # conn.write resp # ...
63
+ # conn.close
64
+ # end
65
+ # end
66
+ def server_create_connection(title, port, proto: :tcp, bind: '0.0.0.0', shared: true, backlog: nil, **socket_options, &block)
67
+ raise ArgumentError, "BUG: title must be a symbol" unless title && title.is_a?(Symbol)
68
+ raise ArgumentError, "BUG: port must be an integer" unless port && port.is_a?(Integer)
69
+ raise ArgumentError, "BUG: invalid protocol name" unless PROTOCOLS.include?(proto)
70
+ raise ArgumentError, "BUG: cannot create connection for UDP" unless CONNECTION_PROTOCOLS.include?(proto)
71
+
72
+ raise ArgumentError, "BUG: block not specified which handles connection" unless block_given?
73
+ raise ArgumentError, "BUG: block must have just one argument" unless block.arity == 1
74
+
75
+ if proto == :tcp || proto == :tls # default linger_timeout only for server
76
+ socket_options[:linger_timeout] ||= 0
77
+ end
78
+
79
+ socket_option_validate!(proto, **socket_options)
80
+ socket_option_setter = ->(sock){ socket_option_set(sock, **socket_options) }
81
+
82
+ case proto
83
+ when :tcp
84
+ server = server_create_for_tcp_connection(shared, bind, port, backlog, socket_option_setter, &block)
85
+ when :tls
86
+ raise ArgumentError, "BUG: certopts (certificate options) not specified for TLS" unless certopts
87
+ # server_certopts_validate!(certopts)
88
+ # sock = server_create_tls_socket(shared, bind, port)
89
+ # server = nil # ...
90
+ raise "not implemented yet"
91
+ when :unix
92
+ raise "not implemented yet"
93
+ else
94
+ raise "unknown protocol #{proto}"
95
+ end
96
+
97
+ server_attach(title, proto, port, bind, shared, server)
98
+ end
99
+
100
+ # server_create(:title, @port) do |data|
101
+ # # ...
102
+ # end
103
+ # server_create(:title, @port) do |data, conn|
104
+ # # ...
105
+ # end
106
+ # server_create(:title, @port, proto: :udp, max_bytes: 2048) do |data, sock|
107
+ # sock.remote_host
108
+ # sock.remote_port
109
+ # # ...
110
+ # end
111
+ def server_create(title, port, proto: :tcp, bind: '0.0.0.0', shared: true, socket: nil, backlog: nil, max_bytes: nil, flags: 0, **socket_options, &callback)
112
+ raise ArgumentError, "BUG: title must be a symbol" unless title && title.is_a?(Symbol)
113
+ raise ArgumentError, "BUG: port must be an integer" unless port && port.is_a?(Integer)
114
+ raise ArgumentError, "BUG: invalid protocol name" unless PROTOCOLS.include?(proto)
115
+
116
+ raise ArgumentError, "BUG: socket option is available only for udp" if socket && proto != :udp
117
+
118
+ raise ArgumentError, "BUG: block not specified which handles received data" unless block_given?
119
+ raise ArgumentError, "BUG: block must have 1 or 2 arguments" unless callback.arity == 1 || callback.arity == 2
120
+
121
+ if proto == :tcp || proto == :tls # default linger_timeout only for server
122
+ socket_options[:linger_timeout] ||= 0
123
+ end
124
+
125
+ unless socket
126
+ socket_option_validate!(proto, **socket_options)
127
+ socket_option_setter = ->(sock){ socket_option_set(sock, **socket_options) }
128
+ end
129
+
130
+ if proto != :tcp && proto != :tls && proto != :unix # options to listen/accept connections
131
+ raise ArgumentError, "BUG: backlog is available for tcp/tls" if backlog
132
+ end
133
+ if proto != :udp # UDP options
134
+ raise ArgumentError, "BUG: max_bytes is available only for udp" if max_bytes
135
+ raise ArgumentError, "BUG: flags is available only for udp" if flags != 0
136
+ end
137
+
138
+ case proto
139
+ when :tcp
140
+ server = server_create_for_tcp_connection(shared, bind, port, backlog, socket_option_setter) do |conn|
141
+ conn.data(&callback)
142
+ end
143
+ when :tls
144
+ raise "not implemented yet"
145
+ when :udp
146
+ raise ArgumentError, "BUG: max_bytes must be specified for UDP" unless max_bytes
147
+ if socket
148
+ sock = socket
149
+ close_socket = false
150
+ else
151
+ sock = server_create_udp_socket(shared, bind, port)
152
+ socket_option_setter.call(sock)
153
+ close_socket = true
154
+ end
155
+ server = EventHandler::UDPServer.new(sock, max_bytes, flags, close_socket, @log, @under_plugin_development, &callback)
156
+ when :unix
157
+ raise "not implemented yet"
158
+ else
159
+ raise "BUG: unknown protocol #{proto}"
160
+ end
161
+
162
+ server_attach(title, proto, port, bind, shared, server)
163
+ end
164
+
165
+ def server_create_tcp(title, port, **kwargs, &callback)
166
+ server_create(title, port, proto: :tcp, **kwargs, &callback)
167
+ end
168
+
169
+ def server_create_udp(title, port, **kwargs, &callback)
170
+ server_create(title, port, proto: :udp, **kwargs, &callback)
171
+ end
172
+
173
+ def server_create_tls(title, port, **kwargs, &callback)
174
+ server_create(title, port, proto: :tls, **kwargs, &callback)
175
+ end
176
+
177
+ def server_create_unix(title, port, **kwargs, &callback)
178
+ server_create(title, port, proto: :unix, **kwargs, &callback)
179
+ end
180
+
181
+ ServerInfo = Struct.new(:title, :proto, :port, :bind, :shared, :server)
182
+
183
+ def server_attach(title, proto, port, bind, shared, server)
184
+ @_servers << ServerInfo.new(title, proto, port, bind, shared, server)
185
+ event_loop_attach(server)
186
+ end
187
+
188
+ def server_create_for_tcp_connection(shared, bind, port, backlog, socket_option_setter, &block)
189
+ sock = server_create_tcp_socket(shared, bind, port)
190
+ socket_option_setter.call(sock)
191
+ close_callback = ->(conn){ @_server_mutex.synchronize{ @_server_connections.delete(conn) } }
192
+ server = Coolio::TCPServer.new(sock, nil, EventHandler::TCPServer, socket_option_setter, close_callback, @log, @under_plugin_development, block) do |conn|
193
+ @_server_mutex.synchronize do
194
+ @_server_connections << conn
195
+ end
196
+ end
197
+ server.listen(backlog) if backlog
198
+ server
199
+ end
200
+
201
+ def initialize
202
+ super
203
+ @_servers = []
204
+ @_server_connections = []
205
+ @_server_mutex = Mutex.new
206
+ end
207
+
208
+ def shutdown
209
+ @_server_connections.each do |conn|
210
+ conn.close rescue nil
211
+ end
212
+ @_server_mutex.synchronize do
213
+ @_servers.each do |si|
214
+ si.server.detach if si.server.attached?
215
+ end
216
+ end
217
+
218
+ super
219
+ end
220
+
221
+ def close
222
+ @_server_connections.each do |conn|
223
+ conn.close rescue nil
224
+ end
225
+ @_server_mutex.synchronize do
226
+ @_servers.each do |si|
227
+ si.server.close rescue nil
228
+ end
229
+ end
230
+ super
231
+ end
232
+
233
+ def terminate
234
+ @_servers = []
235
+ super
236
+ end
237
+
238
+ def server_certopts_validate!(certopts)
239
+ raise "not implemented yet"
240
+ end
241
+
242
+ def server_socket_manager_client
243
+ socket_manager_path = ENV['SERVERENGINE_SOCKETMANAGER_PATH']
244
+ if Fluent.windows?
245
+ socket_manager_path = socket_manager_path.to_i
246
+ end
247
+ ServerEngine::SocketManager::Client.new(socket_manager_path)
248
+ end
249
+
250
+ def server_create_tcp_socket(shared, bind, port)
251
+ sock = if shared
252
+ server_socket_manager_client.listen_tcp(bind, port)
253
+ else
254
+ TCPServer.new(bind, port) # this method call can create sockets for AF_INET6
255
+ end
256
+ # close-on-exec is set by default in Ruby 2.0 or later (, and it's unavailable on Windows)
257
+ sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK) # nonblock
258
+ sock
259
+ end
260
+
261
+ def server_create_udp_socket(shared, bind, port)
262
+ sock = if shared
263
+ server_socket_manager_client.listen_udp(bind, port)
264
+ else
265
+ family = IPAddr.new(IPSocket.getaddress(bind)).ipv4? ? ::Socket::AF_INET : ::Socket::AF_INET6
266
+ usock = UDPSocket.new(family)
267
+ usock.bind(bind, port)
268
+ usock
269
+ end
270
+ # close-on-exec is set by default in Ruby 2.0 or later (, and it's unavailable on Windows)
271
+ sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK) # nonblock
272
+ sock
273
+ end
274
+
275
+ def server_create_tls_socket(shared, bind, port)
276
+ raise "not implemented yet"
277
+ end
278
+
279
+ class CallbackSocket
280
+ def initialize(server_type, sock, enabled_events = [], close_socket: true)
281
+ @server_type = server_type
282
+ @sock = sock
283
+ @enabled_events = enabled_events
284
+ @close_socket = close_socket
285
+ end
286
+
287
+ def remote_addr
288
+ @sock.peeraddr[3]
289
+ end
290
+
291
+ def remote_host
292
+ @sock.peeraddr[2]
293
+ end
294
+
295
+ def remote_port
296
+ @sock.peeraddr[1]
297
+ end
298
+
299
+ def send(data, flags = 0)
300
+ @sock.send(data, flags)
301
+ end
302
+
303
+ def write(data)
304
+ raise "not implemented here"
305
+ end
306
+
307
+ def close
308
+ @sock.close if @close_socket
309
+ end
310
+
311
+ def data(&callback)
312
+ on(:data, &callback)
313
+ end
314
+
315
+ def on(event, &callback)
316
+ raise "BUG: this event is disabled for #{@server_type}: #{event}" unless @enabled_events.include?(event)
317
+ case event
318
+ when :data
319
+ @sock.data(&callback)
320
+ when :write_complete
321
+ cb = ->(){ callback.call(self) }
322
+ @sock.on_write_complete(&cb)
323
+ when :close
324
+ cb = ->(){ callback.call(self) }
325
+ @sock.on_close(&cb)
326
+ else
327
+ raise "BUG: unknown event: #{event}"
328
+ end
329
+ end
330
+ end
331
+
332
+ class TCPCallbackSocket < CallbackSocket
333
+ def initialize(sock)
334
+ super("tcp", sock, [:data, :write_complete, :close])
335
+ end
336
+
337
+ def write(data)
338
+ @sock.write(data)
339
+ end
340
+ end
341
+
342
+ class UDPCallbackSocket < CallbackSocket
343
+ def initialize(sock, peeraddr, **kwargs)
344
+ super("udp", sock, [], **kwargs)
345
+ @peeraddr = peeraddr
346
+ end
347
+
348
+ def remote_addr
349
+ @peeraddr[3]
350
+ end
351
+
352
+ def remote_host
353
+ @peeraddr[2]
354
+ end
355
+
356
+ def remote_port
357
+ @peeraddr[1]
358
+ end
359
+
360
+ def write(data)
361
+ @sock.send(data, 0, @peeraddr[3], @peeraddr[1])
362
+ end
363
+ end
364
+
365
+ module EventHandler
366
+ class UDPServer < Coolio::IO
367
+ def initialize(sock, max_bytes, flags, close_socket, log, under_plugin_development, &callback)
368
+ raise ArgumentError, "socket must be a UDPSocket: sock = #{sock}" unless sock.is_a?(UDPSocket)
369
+
370
+ super(sock)
371
+
372
+ @sock = sock
373
+ @max_bytes = max_bytes
374
+ @flags = flags
375
+ @close_socket = close_socket
376
+ @log = log
377
+ @under_plugin_development = under_plugin_development
378
+ @callback = callback
379
+
380
+ on_readable_impl = case @callback.arity
381
+ when 1 then :on_readable_without_sock
382
+ when 2 then :on_readable_with_sock
383
+ else
384
+ raise "BUG: callback block must have 1 or 2 arguments"
385
+ end
386
+ self.define_singleton_method(:on_readable, method(on_readable_impl))
387
+ end
388
+
389
+ def on_readable_without_sock
390
+ begin
391
+ data = @sock.recv(@max_bytes, @flags)
392
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR, Errno::ECONNRESET
393
+ return
394
+ end
395
+ @callback.call(data)
396
+ rescue => e
397
+ @log.error "unexpected error in processing UDP data", error: e
398
+ @log.error_backtrace
399
+ raise if @under_plugin_development
400
+ end
401
+
402
+ def on_readable_with_sock
403
+ begin
404
+ data, addr = @sock.recvfrom(@max_bytes)
405
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR, Errno::ECONNRESET
406
+ return
407
+ end
408
+ @callback.call(data, UDPCallbackSocket.new(@sock, addr, close_socket: @close_socket))
409
+ rescue => e
410
+ @log.error "unexpected error in processing UDP data", error: e
411
+ @log.error_backtrace
412
+ raise if @under_plugin_development
413
+ end
414
+ end
415
+
416
+ class TCPServer < Coolio::TCPSocket
417
+ def initialize(sock, socket_option_setter, close_callback, log, under_plugin_development, connect_callback)
418
+ raise ArgumentError, "socket must be a TCPSocket: sock=#{sock}" unless sock.is_a?(TCPSocket)
419
+
420
+ socket_option_setter.call(sock)
421
+
422
+ @_handler_socket = sock
423
+ super(sock)
424
+
425
+ @log = log
426
+ @under_plugin_development = under_plugin_development
427
+
428
+ @connect_callback = connect_callback
429
+ @data_callback = nil
430
+ @close_callback = close_callback
431
+
432
+ @callback_connection = nil
433
+ @closing = false
434
+
435
+ @mutex = Mutex.new # to serialize #write and #close
436
+ end
437
+
438
+ def data(&callback)
439
+ raise "data callback can be registered just once, but registered twice" if self.singleton_methods.include?(:on_read)
440
+ @data_callback = callback
441
+ on_read_impl = case callback.arity
442
+ when 1 then :on_read_without_connection
443
+ when 2 then :on_read_with_connection
444
+ else
445
+ raise "BUG: callback block must have 1 or 2 arguments"
446
+ end
447
+ self.define_singleton_method(:on_read, method(on_read_impl))
448
+ end
449
+
450
+ def write(data)
451
+ @mutex.synchronize do
452
+ super
453
+ end
454
+ end
455
+
456
+ def on_connect
457
+ @callback_connection = TCPCallbackSocket.new(self)
458
+ @connect_callback.call(@callback_connection)
459
+ unless @data_callback
460
+ raise "connection callback must call #data to set data callback"
461
+ end
462
+ end
463
+
464
+ def on_read_without_connection(data)
465
+ @data_callback.call(data)
466
+ rescue => e
467
+ @log.error "unexpected error on reading data", host: remote_host, port: remote_port, error: e
468
+ @log.error_backtrace
469
+ close(true) rescue nil
470
+ raise if @under_plugin_development
471
+ end
472
+
473
+ def on_read_with_connection(data)
474
+ @data_callback.call(data, @callback_connection)
475
+ rescue => e
476
+ @log.error "unexpected error on reading data", host: remote_host, port: remote_port, error: e
477
+ @log.error_backtrace
478
+ close(true) rescue nil
479
+ raise if @under_plugin_development
480
+ end
481
+
482
+ def close
483
+ @mutex.synchronize do
484
+ return if @closing
485
+ @closing = true
486
+ @close_callback.call(self)
487
+ super
488
+ end
489
+ end
490
+ end
491
+ end
492
+ end
493
+ end
494
+ end