Search code examples
ruby-on-railsrubyajaxfilefile-read

How to read a file block in Rails without read it again from beginning


I have a growing file (log) that I need to read by blocks. I make a call by Ajax to get a specified number of lines. I used File.foreach to read the lines I want, but it reads always from the beginning and I need to return only the lines I want, directly.

Example Pseudocode:

 #First call:
  File.open and return 0 to 10 lines

 #Second call:
  File.open and return 11 to 20 lines

 #Third call:
  File.open and return 21 to 30 lines

 #And so on...

Is there anyway to make this?


Solution

  • Solution 1: Reading the whole file

    The proposed solution here:
    https://stackoverflow.com/a/5052929/1433751

    ..is not an efficient solution in your case, because it requires you to read all the lines from the file for each AJAX request, even if you just need the last 10 lines of the logfile.

    That's an enormous waste of time, and in computing terms the solving time (i.e. process the whole logfile in blocks of size N) approaches exponential solving time.

    Solution 2: Seeking

    Since your AJAX calls request sequential lines we can implement a much more efficient approach by seeking to the correct position before reading, using IO.seek and IO.pos.

    This requires you to return some extra data (the last file position) back to the AJAX client at the same time you return the requested lines.

    The AJAX request then becomes a function call of this form request_lines(position, line_count), which enables the server to IO.seek(position) before reading the requested count of lines.

    Here's the pseudocode for the solution:

    Client code:

    LINE_COUNT = 10
    pos = 0
    
    loop {
      data = server.request_lines(pos, LINE_COUNT)
      display_lines(data.lines)
      pos = data.pos
      break if pos == -1  # Reached end of file
    }
    

    Server code:

    def request_lines(pos, line_count)
      file = File.open('logfile')
    
      # Seek to requested position
      file.seek(pos)
    
      # Read the requested count of lines while checking for EOF
      lines = count.times.map { file.readline if !file.eof? }.compact
    
      # Mark pos with -1 if we reached EOF during reading
      pos = file.eof? ? -1 : file.pos
      f.close
    
      # Return data
      data = { lines: lines, pos: pos }
    end