module Travis
  class Worker
    module Shell
      class Buffer < String
        attr_reader :pos, :interval, :limit, :callback

        def initialize(interval = nil, options = {}, &callback)
          @interval = interval
          @callback = callback
          @limit = options[:limit] || Travis::Worker.config.limits.log_length
          @pos = 0

          start if interval
        end

        def <<(other)
          super.tap do
            limit_exeeded! if length > limit
          end
        end

        def flush
          read.tap do |string|
            callback.call(string) if callback
          end if !empty?
        end

        def empty?
          pos == length
        end

        protected

          def read
            string = self[pos, length - pos]
            @pos += string.length
            string
          end

          def start
            @thread = Thread.new do
              loop do
                flush
                sleep(interval) if interval
              end
            end
          end

          def limit_exeeded!
            raise Travis::Build::OutputLimitExceeded.new(limit.to_s)
          end
      end
    end
  end
end

module Travis
  class Worker
    module Shell
      class Buffer < String
        attr_reader :pos, :interval, :limit, :callback

        def initialize(interval = nil, options = {}, &callback)
          @interval = interval
          @callback = callback
          @limit = options[:limit] || Travis::Worker.config.limits.log_length
          @pos = 0

          start if interval
        end

        def <<(other)
          super.tap do
            # make sure limit is initialized and > 0. Appending here happens
            # asynchronously and #initialize may or may not have finished running
            # by then. In addition, #length here is a regular method which is not
            # synchronized. All this leads to #limit_exeeded! being called
            # too early (and this explains build logs w/o any output but this length limit
            # system message). MK.
            limit_exeeded! if @limit && (@limit > 0) && length > @limit
          end
        end

        def flush
          read.tap do |string|
            callback.call(string) if callback
          end if !empty?
        end

        def empty?
          pos == length
        end

        protected

          def read
            string = self[pos, length - pos]
            @pos += string.length
            string
          end

          def start
            @thread = Thread.new do
              loop do
                flush
                sleep(interval) if interval
              end
            end
          end

          def limit_exeeded!
            raise Travis::Build::OutputLimitExceeded.new(limit.to_s)
          end
      end
    end
  end
end

Check that limit is initialized and is > 0 before raising log length limit exceptions
Check that limit is initialized and is > 0 before raising log length limit exceptions

Buffer#<< is called asynchronously by a separate thread Net::SSH is using. Buffer#initialize may or may not happen by then.
This causes build to be immediately terminated w/o log output some of the time, exactly what people have been reporting
for the last day or so.

As a side note, previous implementation used one condition and 2 methods in the output callback. Log length was initialized
before SSH session was started. Now we have a String subclass, super.tap, lazy config initialization and exceptions.
Maybe movign back to the previous approach will make things easier to reason about?
