pitchfork 0.12.0 → 0.14.0

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -3
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Dockerfile +1 -1
  6. data/Gemfile.lock +3 -5
  7. data/benchmark/README.md +1 -1
  8. data/benchmark/cow_benchmark.rb +1 -0
  9. data/docs/CONFIGURATION.md +39 -1
  10. data/docs/MIGRATING_FROM_UNICORN.md +34 -0
  11. data/docs/WHY_MIGRATE.md +5 -0
  12. data/examples/constant_caches.ru +1 -0
  13. data/examples/echo.ru +1 -0
  14. data/examples/hello.ru +1 -0
  15. data/examples/pitchfork.conf.minimal.rb +1 -0
  16. data/examples/pitchfork.conf.rb +1 -0
  17. data/examples/pitchfork.conf.service.rb +27 -0
  18. data/exe/pitchfork +5 -4
  19. data/ext/pitchfork_http/epollexclusive.h +2 -2
  20. data/ext/pitchfork_http/extconf.rb +3 -0
  21. data/ext/pitchfork_http/memory_page.c +223 -0
  22. data/ext/pitchfork_http/pitchfork_http.c +213 -211
  23. data/ext/pitchfork_http/pitchfork_http.rl +3 -1
  24. data/lib/pitchfork/children.rb +21 -15
  25. data/lib/pitchfork/configurator.rb +13 -0
  26. data/lib/pitchfork/const.rb +1 -0
  27. data/lib/pitchfork/flock.rb +1 -0
  28. data/lib/pitchfork/http_parser.rb +18 -72
  29. data/lib/pitchfork/http_response.rb +4 -3
  30. data/lib/pitchfork/http_server.rb +181 -62
  31. data/lib/pitchfork/launcher.rb +1 -0
  32. data/lib/pitchfork/message.rb +11 -6
  33. data/lib/pitchfork/select_waiter.rb +1 -0
  34. data/lib/pitchfork/shared_memory.rb +16 -14
  35. data/lib/pitchfork/socket_helper.rb +2 -1
  36. data/lib/pitchfork/stream_input.rb +6 -5
  37. data/lib/pitchfork/tee_input.rb +3 -2
  38. data/lib/pitchfork/tmpio.rb +1 -0
  39. data/lib/pitchfork/version.rb +1 -1
  40. data/lib/pitchfork/worker.rb +44 -15
  41. data/lib/pitchfork.rb +1 -20
  42. data/pitchfork.gemspec +0 -1
  43. metadata +7 -18
  44. data/lib/pitchfork/app/old_rails/static.rb +0 -59
@@ -1,4 +1,6 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
3
+
2
4
  require 'pitchfork/pitchfork_http'
3
5
  require 'pitchfork/flock'
4
6
  require 'pitchfork/soft_timeout'
@@ -75,7 +77,7 @@ module Pitchfork
75
77
 
76
78
  # :stopdoc:
77
79
  attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
78
- :before_fork, :after_worker_fork, :after_mold_fork,
80
+ :before_fork, :after_worker_fork, :after_mold_fork, :before_service_worker_ready, :before_service_worker_exit,
79
81
  :listener_opts, :children,
80
82
  :orig_app, :config, :ready_pipe,
81
83
  :default_middleware, :early_hints
@@ -91,33 +93,7 @@ module Pitchfork
91
93
  # in new projects
92
94
  LISTENERS = []
93
95
 
94
- NOOP = '.'.freeze
95
-
96
- # :startdoc:
97
- # This Hash is considered a stable interface and changing its contents
98
- # will allow you to switch between different installations of Pitchfork
99
- # or even different installations of the same applications without
100
- # downtime. Keys of this constant Hash are described as follows:
101
- #
102
- # * 0 - the path to the pitchfork executable
103
- # * :argv - a deep copy of the ARGV array the executable originally saw
104
- # * :cwd - the working directory of the application, this is where
105
- # you originally started Pitchfork.
106
- # TODO: Can we get rid of this?
107
- START_CTX = {
108
- :argv => ARGV.map(&:dup),
109
- 0 => $0.dup,
110
- }
111
- # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
112
- # and like systems
113
- START_CTX[:cwd] = begin
114
- a = File.stat(pwd = ENV['PWD'])
115
- b = File.stat(Dir.pwd)
116
- a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
117
- rescue
118
- Dir.pwd
119
- end
120
- # :stopdoc:
96
+ NOOP = '.'
121
97
 
122
98
  # Creates a working server on host:port (strange things happen if
123
99
  # port isn't a Number). Use HttpServer::run to start the server and
@@ -138,7 +114,7 @@ module Pitchfork
138
114
  self.config = Pitchfork::Configurator.new(options)
139
115
  self.listener_opts = {}
140
116
 
141
- proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
117
+ proc_name role: 'monitor', status: ARGV.join(' ')
142
118
 
143
119
  # We use @control_socket differently in the master and worker processes:
144
120
  #
@@ -171,7 +147,7 @@ module Pitchfork
171
147
  :QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
172
148
 
173
149
  Info.workers_count = worker_processes
174
- SharedMemory.preallocate_drops(worker_processes)
150
+ SharedMemory.preallocate_pages(worker_processes)
175
151
  end
176
152
 
177
153
  # Runs the thing. Returns self so you can run join on it
@@ -294,7 +270,7 @@ module Pitchfork
294
270
  def join
295
271
  @respawn = true
296
272
 
297
- proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
273
+ proc_name role: 'monitor', status: ARGV.join(' ')
298
274
 
299
275
  logger.info "master process ready" # test_exec.rb relies on this message
300
276
  if @ready_pipe
@@ -372,6 +348,9 @@ module Pitchfork
372
348
  when Message::MoldSpawned
373
349
  new_mold = @children.update(message)
374
350
  logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
351
+ when Message::ServiceSpawned
352
+ new_service = @children.update(message)
353
+ logger.info("service pid=#{new_service.pid} gen=#{new_service.generation} spawned")
375
354
  when Message::MoldReady
376
355
  old_molds = @children.molds
377
356
  new_mold = @children.update(message)
@@ -427,6 +406,20 @@ module Pitchfork
427
406
  Process.exit
428
407
  end
429
408
 
409
+ def service_exit(service)
410
+ logger.info "service pid=#{service.pid} gen=#{service.generation} exiting"
411
+ proc_name status: "exiting"
412
+
413
+ if @before_service_worker_exit
414
+ begin
415
+ @before_service_worker_exit.call(self, service)
416
+ rescue => error
417
+ Pitchfork.log_error(logger, "before_service_worker_exit error", error)
418
+ end
419
+ end
420
+ Process.exit
421
+ end
422
+
430
423
  def rewindable_input
431
424
  Pitchfork::HttpParser.input_class.method_defined?(:rewind)
432
425
  end
@@ -512,6 +505,7 @@ module Pitchfork
512
505
  next
513
506
  else # worker is out of time
514
507
  next_sleep = 0
508
+ child.deadline = now + 1
515
509
  hard_timeout(child)
516
510
  end
517
511
  end
@@ -587,6 +581,69 @@ module Pitchfork
587
581
  worker
588
582
  end
589
583
 
584
+ def service_loop(service)
585
+ readers = init_service_process(service)
586
+ waiter = prep_readers(readers)
587
+
588
+ ready = readers.dup
589
+
590
+ if @before_service_worker_ready
591
+ begin
592
+ @before_service_worker_ready.call(self, service)
593
+ rescue => error
594
+ Pitchfork.log_error(logger, "before_service_worker_ready", error)
595
+ Process.exit(1)
596
+ end
597
+ end
598
+
599
+ proc_name status: "ready"
600
+
601
+ while readers[0]
602
+ begin
603
+ service.update_deadline(@timeout)
604
+ while sock = ready.shift
605
+ # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
606
+ # but that will return false
607
+ client = sock.accept_nonblock(exception: false)
608
+
609
+ case client
610
+ when false, :wait_readable
611
+ # no message, keep looping
612
+ end
613
+
614
+ service.update_deadline(@timeout)
615
+ end
616
+
617
+ # timeout so we can update .deadline and keep parent from SIGKILL-ing us
618
+ service.update_deadline(@timeout)
619
+
620
+
621
+ waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
622
+ rescue => e
623
+ Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
624
+ end
625
+ end
626
+ end
627
+
628
+ def spawn_service(service, detach:)
629
+ logger.info("service gen=#{service.generation} spawning...")
630
+
631
+ # We set the deadline before spawning the child so that if for some
632
+ # reason it gets stuck before reaching the worker loop,
633
+ # the monitor process will kill it.
634
+ service.update_deadline(@spawn_timeout)
635
+ @before_fork&.call(self)
636
+ fork_sibling("spawn_service") do
637
+ service.pid = Process.pid
638
+
639
+ after_fork_internal
640
+ service_loop(service)
641
+ service_exit(service)
642
+ end
643
+
644
+ service
645
+ end
646
+
590
647
  def spawn_initial_mold
591
648
  mold = Worker.new(nil)
592
649
  mold.create_socketpair!
@@ -603,6 +660,21 @@ module Pitchfork
603
660
  end
604
661
 
605
662
  def spawn_missing_workers
663
+ if @before_service_worker_ready && !@children.service
664
+ service = Pitchfork::Service.new
665
+ if REFORKING_AVAILABLE
666
+ service.generation = @children.mold&.generation || 0
667
+
668
+ unless @children.mold&.spawn_service(service)
669
+ @logger.error("Failed to send a spawn_service command")
670
+ end
671
+ else
672
+ spawn_service(service, detach: false)
673
+ end
674
+
675
+ @children.register_service(service)
676
+ end
677
+
606
678
  worker_nr = -1
607
679
  until (worker_nr += 1) == @worker_processes
608
680
  if @children.nr_alive?(worker_nr)
@@ -640,9 +712,18 @@ module Pitchfork
640
712
  end
641
713
 
642
714
  def maintain_worker_count
643
- (off = @children.workers_count - worker_processes) == 0 and return
644
- off < 0 and return spawn_missing_workers
645
- @children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:TERM) }
715
+ off = @children.workers_count - worker_processes
716
+ off -= 1 if @before_service_worker_ready && !@children.service
717
+
718
+ if off < 0
719
+ spawn_missing_workers
720
+ elsif off > 0
721
+ @children.each_worker do |worker|
722
+ if worker.nr >= worker_processes
723
+ worker.soft_kill(:TERM)
724
+ end
725
+ end
726
+ end
646
727
  end
647
728
 
648
729
  def restart_outdated_workers
@@ -655,6 +736,16 @@ module Pitchfork
655
736
  max_pending_workers = (worker_processes * 0.1).ceil
656
737
  workers_to_restart = max_pending_workers - @children.restarting_workers_count
657
738
 
739
+ if service = @children.service
740
+ if service.outdated?
741
+ if service.soft_kill(:TERM)
742
+ logger.info("Sent SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
743
+ else
744
+ logger.info("Failed to send SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
745
+ end
746
+ end
747
+ end
748
+
658
749
  if workers_to_restart > 0
659
750
  outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
660
751
  outdated_workers.each do |worker|
@@ -696,21 +787,17 @@ module Pitchfork
696
787
 
697
788
  def e103_response_write(client, headers)
698
789
  rss = @request.response_start_sent
699
- buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n"
790
+ buf = (rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n").b
700
791
  headers.each { |key, value| append_header(buf, key, value) }
701
- buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze)
792
+ buf << (rss ? "\r\nHTTP/1.1 " : "\r\n")
702
793
  client.write(buf)
703
794
  end
704
795
 
705
796
  def e100_response_write(client, env)
706
- # We use String#freeze to avoid allocations under Ruby 2.1+
707
- # Not many users hit this code path, so it's better to reduce the
708
- # constant table sizes even for Ruby 2.0 users who'll hit extra
709
- # allocations here.
710
797
  client.write(@request.response_start_sent ?
711
- "100 Continue\r\n\r\nHTTP/1.1 ".freeze :
712
- "HTTP/1.1 100 Continue\r\n\r\n".freeze)
713
- env.delete('HTTP_EXPECT'.freeze)
798
+ "100 Continue\r\n\r\nHTTP/1.1 " :
799
+ "HTTP/1.1 100 Continue\r\n\r\n")
800
+ env.delete('HTTP_EXPECT')
714
801
  end
715
802
 
716
803
  # once a client is accepted, it is processed in its entirety here
@@ -720,7 +807,11 @@ module Pitchfork
720
807
  @request = Pitchfork::HttpParser.new
721
808
  env = @request.read(client)
722
809
 
723
- proc_name status: "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
810
+ status = "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
811
+ if request_id = env["HTTP_X_REQUEST_ID"]
812
+ status += ", request_id: #{request_id}"
813
+ end
814
+ proc_name status: status
724
815
 
725
816
  env["pitchfork.worker"] = worker
726
817
  timeout_handler.rack_env = env
@@ -814,6 +905,18 @@ module Pitchfork
814
905
  readers
815
906
  end
816
907
 
908
+ def init_service_process(service)
909
+ proc_name role: "(gen:#{service.generation}) mold", status: "init"
910
+ LISTENERS.each(&:close) # Don't appear as listening to incoming requests
911
+ service.register_to_master(@control_socket[1])
912
+ readers = [service]
913
+ trap(:QUIT) { nuke_listeners!(readers) }
914
+ trap(:TERM) { nuke_listeners!(readers) }
915
+ trap(:INT) { nuke_listeners!(readers); exit!(0) }
916
+ proc_name role: "(gen:#{service.generation}) service", status: "ready"
917
+ readers
918
+ end
919
+
817
920
  def init_mold_process(mold)
818
921
  proc_name role: "(gen:#{mold.generation}) mold", status: "init"
819
922
  after_mold_fork.call(self, mold)
@@ -827,7 +930,7 @@ module Pitchfork
827
930
 
828
931
  if Pitchfork.const_defined?(:Waiter)
829
932
  def prep_readers(readers)
830
- Pitchfork::Waiter.prep_readers(readers)
933
+ Pitchfork::Info.keep_io(Pitchfork::Waiter.prep_readers(readers))
831
934
  end
832
935
  else
833
936
  require_relative 'select_waiter'
@@ -855,24 +958,25 @@ module Pitchfork
855
958
  # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
856
959
  # but that will return false
857
960
  client = sock.accept_nonblock(exception: false)
858
- client = false if client == :wait_readable
859
- if client
860
- case client
861
- when Message::PromoteWorker
862
- if Info.fork_safe?
863
- spawn_mold(worker)
864
- else
865
- logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
866
- end
867
- when Message
868
- worker.update(client)
961
+
962
+ case client
963
+ when false, :wait_readable
964
+ # no message, keep looping
965
+ when Message::PromoteWorker
966
+ if Info.fork_safe?
967
+ spawn_mold(worker)
869
968
  else
870
- request_env = process_client(client, worker, prepare_timeout(worker))
871
- @after_request_complete&.call(self, worker, request_env)
872
- worker.increment_requests_count
969
+ logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
873
970
  end
874
- worker.update_deadline(@timeout)
971
+ when Message
972
+ worker.update(client)
973
+ else
974
+ request_env = process_client(client, worker, prepare_timeout(worker))
975
+ worker.increment_requests_count
976
+ @after_request_complete&.call(self, worker, request_env)
875
977
  end
978
+
979
+ worker.update_deadline(@timeout)
876
980
  end
877
981
 
878
982
  # timeout so we can update .deadline and keep parent from SIGKILL-ing us
@@ -955,6 +1059,22 @@ module Pitchfork
955
1059
  rescue => error
956
1060
  raise BootFailure, error.message
957
1061
  end
1062
+ when Message::SpawnService
1063
+ retries = 1
1064
+ begin
1065
+ spawn_service(Service.new(generation: mold.generation), detach: true)
1066
+ rescue ForkFailure
1067
+ if retries > 0
1068
+ @logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service. Retrying.")
1069
+ retries -= 1
1070
+ retry
1071
+ else
1072
+ @logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service twice in a row. Corrupted mold process?")
1073
+ Process.exit(1)
1074
+ end
1075
+ rescue => error
1076
+ raise BootFailure, error.message
1077
+ end
958
1078
  else
959
1079
  logger.error("Unexpected mold message #{message.inspect}")
960
1080
  end
@@ -1001,7 +1121,7 @@ module Pitchfork
1001
1121
  @proctitle_role = role if role
1002
1122
  @proctitle_status = status if status
1003
1123
 
1004
- Process.setproctitle("#{File.basename(START_CTX[0])} #{@proctitle_role} - #{@proctitle_status}")
1124
+ Process.setproctitle("#{File.basename($PROGRAM_NAME)} #{@proctitle_role} - #{@proctitle_status}")
1005
1125
  end
1006
1126
 
1007
1127
  def bind_listeners!
@@ -1009,7 +1129,6 @@ module Pitchfork
1009
1129
  if listeners.empty?
1010
1130
  listeners << Pitchfork::Const::DEFAULT_LISTEN
1011
1131
  @init_listeners << Pitchfork::Const::DEFAULT_LISTEN
1012
- START_CTX[:argv] << "-l#{Pitchfork::Const::DEFAULT_LISTEN}"
1013
1132
  end
1014
1133
  listeners.each { |addr| listen(addr) }
1015
1134
  raise ArgumentError, "no listeners" if LISTENERS.empty?
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  # :enddoc:
4
5
  $stdout.sync = $stderr.sync = true
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  # :stopdoc:
4
5
  module Pitchfork
@@ -121,12 +122,16 @@ module Pitchfork
121
122
 
122
123
  Message = Class.new(Struct)
123
124
  class Message
124
- SpawnWorker = Message.new(:nr)
125
- WorkerSpawned = Message.new(:nr, :pid, :generation, :pipe)
126
- PromoteWorker = Message.new(:generation)
127
- MoldSpawned = Message.new(:nr, :pid, :generation, :pipe)
128
- MoldReady = Message.new(:nr, :pid, :generation)
125
+ SpawnWorker = new(:nr)
126
+ WorkerSpawned = new(:nr, :pid, :generation, :pipe)
127
+ PromoteWorker = new(:generation)
129
128
 
130
- SoftKill = Message.new(:signum)
129
+ MoldSpawned = new(:nr, :pid, :generation, :pipe)
130
+ MoldReady = new(:nr, :pid, :generation)
131
+
132
+ SpawnService = new(:_) # Struct.new requires at least 1 member on Ruby < 3.3
133
+ ServiceSpawned = new(:pid, :generation, :pipe)
134
+
135
+ SoftKill = new(:signum)
131
136
  end
132
137
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Pitchfork
2
3
  # fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE
3
4
  class SelectWaiter # :nodoc:
@@ -1,40 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'raindrops'
4
-
5
3
  module Pitchfork
6
4
  module SharedMemory
7
5
  extend self
8
6
 
9
- PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
10
7
  CURRENT_GENERATION_OFFSET = 0
11
8
  SHUTDOWN_OFFSET = 1
12
9
  MOLD_TICK_OFFSET = 2
13
10
  MOLD_PROMOTION_TICK_OFFSET = 3
14
- WORKER_TICK_OFFSET = 4
11
+ SERVICE_TICK_OFFSET = 4
12
+ WORKER_TICK_OFFSET = 5
15
13
 
16
- DROPS = [Raindrops.new(PER_DROP)]
14
+ PAGES = [MemoryPage.new(MemoryPage::SLOTS)]
17
15
 
18
16
  def current_generation
19
- DROPS[0][CURRENT_GENERATION_OFFSET]
17
+ PAGES[0][CURRENT_GENERATION_OFFSET]
20
18
  end
21
19
 
22
20
  def current_generation=(value)
23
- DROPS[0][CURRENT_GENERATION_OFFSET] = value
21
+ PAGES[0][CURRENT_GENERATION_OFFSET] = value
24
22
  end
25
23
 
26
24
  def shutting_down!
27
- DROPS[0][SHUTDOWN_OFFSET] = 1
25
+ PAGES[0][SHUTDOWN_OFFSET] = 1
28
26
  end
29
27
 
30
28
  def shutting_down?
31
- DROPS[0][SHUTDOWN_OFFSET] > 0
29
+ PAGES[0][SHUTDOWN_OFFSET] > 0
32
30
  end
33
31
 
34
32
  class Field
35
33
  def initialize(offset)
36
- @drop = DROPS.fetch(offset / PER_DROP)
37
- @offset = offset % PER_DROP
34
+ @drop = PAGES.fetch(offset / MemoryPage::SLOTS)
35
+ @offset = offset % MemoryPage::SLOTS
38
36
  end
39
37
 
40
38
  def value
@@ -54,6 +52,10 @@ module Pitchfork
54
52
  self[MOLD_PROMOTION_TICK_OFFSET]
55
53
  end
56
54
 
55
+ def service_deadline
56
+ self[SERVICE_TICK_OFFSET]
57
+ end
58
+
57
59
  def worker_deadline(worker_nr)
58
60
  self[WORKER_TICK_OFFSET + worker_nr]
59
61
  end
@@ -67,9 +69,9 @@ module Pitchfork
67
69
  #
68
70
  # However this doesn't account for TTIN signals that increase the
69
71
  # number of workers, but we should probably remove that feature too.
70
- def preallocate_drops(workers_count)
71
- 0.upto(((WORKER_TICK_OFFSET + workers_count) / PER_DROP.to_f).ceil) do |i|
72
- DROPS[i] ||= Raindrops.new(PER_DROP)
72
+ def preallocate_pages(workers_count)
73
+ 0.upto(((WORKER_TICK_OFFSET + workers_count) / MemoryPage::SLOTS.to_f).ceil) do |i|
74
+ PAGES[i] ||= MemoryPage.new(MemoryPage::SLOTS)
73
75
  end
74
76
  end
75
77
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
  # :enddoc:
3
4
  require 'socket'
4
5
 
@@ -71,7 +72,7 @@ module Pitchfork
71
72
  rescue => e
72
73
  logger.error("#{sock_name(sock)} " \
73
74
  "failed to set accept_filter=#{name} (#{e.inspect})")
74
- logger.error("perhaps accf_http(9) needs to be loaded".freeze)
75
+ logger.error("perhaps accf_http(9) needs to be loaded")
75
76
  end if arg != got
76
77
  end
77
78
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Pitchfork
4
5
  # When processing uploads, pitchfork may expose a StreamInput object under
@@ -17,7 +18,7 @@ module Pitchfork
17
18
  @socket = socket
18
19
  @parser = request
19
20
  @buf = request.buf
20
- @rbuf = ''
21
+ @rbuf = +''
21
22
  @bytes_read = 0
22
23
  filter_body(@rbuf, @buf) unless @buf.empty?
23
24
  end
@@ -41,7 +42,7 @@ module Pitchfork
41
42
  # ios.read(length [, buffer]) will return immediately if there is
42
43
  # any data and only block when nothing is available (providing
43
44
  # IO#readpartial semantics).
44
- def read(length = nil, rv = '')
45
+ def read(length = nil, rv = ''.b)
45
46
  if length
46
47
  if length <= @rbuf.size
47
48
  length < 0 and raise ArgumentError, "negative length #{length} given"
@@ -79,16 +80,16 @@ module Pitchfork
79
80
  # unlike IO#gets.
80
81
  def gets(sep = $/)
81
82
  if sep.nil?
82
- read_all(rv = '')
83
+ read_all(rv = ''.b)
83
84
  return rv.empty? ? nil : rv
84
85
  end
85
86
  re = /\A(.*?#{Regexp.escape(sep)})/
86
87
 
87
88
  begin
88
- @rbuf.sub!(re, '') and return $1
89
+ @rbuf.sub!(re, ''.b) and return $1
89
90
  return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
90
91
  @socket.readpartial(@@io_chunk_size, @buf) or eof!
91
- filter_body(once = '', @buf)
92
+ filter_body(once = ''.b, @buf)
92
93
  @rbuf << once
93
94
  end while true
94
95
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Pitchfork
4
5
  # Acts like tee(1) on an input input to provide a input-like stream
@@ -42,7 +43,7 @@ module Pitchfork
42
43
  @len = request.content_length
43
44
  super
44
45
  @tmp = @len && @len <= @@client_body_buffer_size ?
45
- StringIO.new("") : new_tmpio
46
+ StringIO.new.binmode : new_tmpio
46
47
  end
47
48
 
48
49
  # :call-seq:
@@ -121,7 +122,7 @@ module Pitchfork
121
122
 
122
123
  # consumes the stream of the socket
123
124
  def consume!
124
- junk = ""
125
+ junk = "".b
125
126
  nil while read(@@io_chunk_size, junk)
126
127
  end
127
128
 
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
  # :stopdoc:
3
4
  require 'tmpdir'
4
5
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.12.0"
4
+ VERSION = "0.14.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end