Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make watch_file into state machine and leverage it throughout #74

Merged
merged 18 commits into from
Jan 27, 2016

Conversation

guyboertje
Copy link
Collaborator

NOTES:

  • Fixes a bug where ignored files were being opened and then closed simply to get it into the sincedb.
  • Fixes a bug where for some ignored files the @sincedb[@statcache[path]] returned nil.
  • Fixes a bug where unlimited number of files can be opened until the available file handles are exhausted - then, for example, the sincedb cannot be written and other bad things begin to happen on the system.

This is a big refactor.

  • WatchedFile instances become the source of truth about a file being watched.
  • The WatchedFile instance will be in only one state, and previous state history can be queried.
  • WatchedFile also holds the last stat taken, the inode and the last read size.
  • WatchedFile also holds the tokeniser buffer and the ruby File instance when it is opened.
  • The Event and WatchedFile instance is now yielded instead of the event and path.
  • Seeks, reads and close also go via the WatchedFile so it can update internals if needed.
  • In Tail the @statcache and separate @files storage hashes are gone.
  • The multiple File::Stat calls are reduced, one during discovery when the WatchFile instance is being constructed and one during the each iteration.
  • The each iteration has been split into state-based sections that iterate over WatchedFiles in that state e.g. closed, ignored, watched, active. A WatchedFile in an unwatched state is never looked at again.
  • the watched iteration takes a limited number of watched_files (4096), makes them active and yields :create or :create_initial but only if the watched_file does not have a history of being closed or ignored before. The implications of this is that if the close_older value is not set to close files then only the first MAX_OPEN_FILES can be tailed. There is an ENV setting for this value as it is set system wide for all instances of filewatch (e.g. multiple file inputs) and an options setting that gives each filewatch instance its own setting.

end

MAX_OPEN_FILES = ENV.fetch("MAX_OPEN_FILES", 4095).to_i
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this an environment variable vs a config?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its both. ENV for when the config is not available to the user.
There are precedents in using both: ENV["SINCEDB_PATH"]

In watch.rb:

...
    @max_active = MAX_OPEN_FILES
...
    def max_open_files=(value)
      return if value.nil?
      val = value.to_i
      @max_active = val <= 0 ? MAX_OPEN_FILES : value
    end

and in tail_base.rb:59

    @watch.max_open_files = @opts[:max_open_files]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it needs a test and a better name because its in the global namespace and it may be overwritten by something else loading later.

@guyboertje guyboertje assigned jsvd and unassigned jordansissel Jan 21, 2016
listener.accept(line)
@sincedb[@statcache[path]] += (line.bytesize + delimiter_byte_size)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to move this? this line is sort of the "ack" after processing. I believe it was purposefully put after the accept to prevent updating the @sincedb then suffer some nasty behaviour in accept and essentially skip that line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. `listener.accept(line) may raise an error. I will revert this.

@jsvd
Copy link
Collaborator

jsvd commented Jan 21, 2016

running the specs failed for me (ruby mri 2.2.1):

  1) FileWatch::Watch when ignore_older is less than close_older and all files are expired yields no file events
     Failure/Error: expect(results).to eq([])

       expected: []
            got: [[:create_initial, "/var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-a6551ca9c3bb5bc6a82109e2dfcce27ad00440664324b4ac6200cb27f1bb/1.log"], [:modify, "/var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-a6551ca9c3bb5bc6a82109e2dfcce27ad00440664324b4ac6200cb27f1bb/1.log"]]

       (compared using ==)

       Diff:
       @@ -1,2 +1,5 @@
       -[]
       +[[:create_initial,
       +  "/var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-a6551ca9c3bb5bc6a82109e2dfcce27ad00440664324b4ac6200cb27f1bb/1.log"],
       + [:modify,
       +  "/var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-a6551ca9c3bb5bc6a82109e2dfcce27ad00440664324b4ac6200cb27f1bb/1.log"]]
     # ./spec/watch_spec.rb:356:in `block (3 levels) in <top (required)>'

NOTE: it is intermittent for me, can't always replicate

Thread.new do
sleep quit_sleep
subject.quit
formatted_puts("subscribing")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is somewhat annoying when running the specs:

~/projects/ruby-filewatch (git)-[pr/74] % bundle exec rspec
....................W, [2016-01-21T10:42:09.185316 #96086]  WARN -- : failed to open /var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-ec52acd1e1143e9af5432021bf1b107bc53c8077cf0a6b11c67cfe9ec5d9: No such file or directory @ rb_sysopen - /var/folders/4g/v6scxgk106dcnlhspm54rl440000gn/T/studtmp-ec52acd1e1143e9af5432021bf1b107bc53c8077cf0a6b11c67cfe9ec5d9
............    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
.    subscribing
F    subscribing
.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can fix this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be fixed now.

@guyboertje
Copy link
Collaborator Author

running the specs failed for me (ruby mri 2.2.1)

I can't get it to fail on MRI 2.2.2.
Perhaps you can try increase line 346 of watch_spec.rb to 2.1.
Tell me if that is better for you.

file_deleteable = []
# look at the closed to see if its changed

@files.values.select {|wf| wf.closed? }.each do |watched_file|
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're worried about object churn here, you can use @files.select {|_, wf| wf.closed? }.each do |_, wf| ... end since Hash#select returns an enumerable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hash#select returns an enumerable

It does and over a new smaller Hash that the each iterates over. What I really want is each_selected_value(proc, block) 😄

end
end

@files.each do |path, watched_file|
# warning on open file limit
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's log the number of files that we can't watch becuse it would exceed @max_active, just to let users know that this is an intentional decision by the library and not a bug.

@jordansissel
Copy link
Owner

@jsvd I ran the specs a few times and wasn't able to reproduce your failure. Bonus points? :P

@jordansissel
Copy link
Owner

Tested the logstash file input just in case, tests pass ✅

% grep filewatch *lock
      filewatch (~> 0.8.0)
  remote: /home/jls/projects/ruby-filewatch
    filewatch (0.8.0)
  filewatch!

% bundle exec rspec
Using Accessor#strict_set for specs
Run options: exclude {:redis=>true, :socket=>true, :performance=>true, :couchdb=>true, :elasticsearch=>true, :elasticsearch_secu
re=>true, :export_cypher=>true, :integration=>true, :windows=>true}
........

Finished in 9.42 seconds (files took 2.62 seconds to load)
8 examples, 0 failures

Randomized with seed 25232

# can be called from different threads.
@lock = Mutex.new
# we need to be threadsafe about the quit mutation
@quit = false
@quit_lock = Mutex.new
@max_active = ENV.fetch("FILEWATCH_MAX_OPEN_FILES", 4095).to_i
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is that this shouldn't be configurable from the environment. However, we can leave it as-is as long as we don't document it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mild refactor this to use self.max_active = ...

@jsvd
Copy link
Collaborator

jsvd commented Jan 27, 2016

LGTM

guyboertje pushed a commit that referenced this pull request Jan 27, 2016
Make watch_file into state machine and leverage it throughout
@guyboertje guyboertje merged commit cf60cb4 into jordansissel:master Jan 27, 2016
@guyboertje guyboertje deleted the watch-fsm branch February 3, 2016 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants