mingle_events 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -114,4 +114,21 @@ Each event that is passed to the processor is an instance of type MingleEvents::
114
114
 
115
115
  h2. Retry & Error handling
116
116
 
117
- As of now, retry is not implemented. If an error occurs during event processing, the error will be logged and processing will stop. The next run will re-start at the point of the last error.
117
+ As of now, retry is not implemented. If an error occurs during event processing, the error will be logged and processing will stop. The next run will re-start at the point of the last error.
118
+
119
+ h2. Historical analysis
120
+
121
+ One of the great usages of event playback is historical analysis. E.g., if you wanted to see the number of stories added each day to your project, you could write a processor that counts new stories and takes a snapshot at the end of each day, where the end of the day is the first time you see an event for the next day. If you are doing this sort of analysis you will not want to poll the entire project's event history every time as that is quite time consuming.
122
+
123
+ It is possible to repeatedly run analysis against previously fetched events using the ProjectEventFetcher class that is only used in the internals of the standard polling mechanism. ProjectEventFetcher does most of the real work of reading new events from the server and making them available for local processing, so it's not a bad class to get to know.
124
+
125
+ <pre>
126
+ event_fetcher = MingleEvents::ProjectEventFetcher.new('mingle', mingle_access)
127
+ processor = MingleEvents::Processors::PutsPublisher.new
128
+ event_fetcher.all_fetched_entries.each do |e|
129
+ processor.process_events([e])
130
+ end
131
+ </pre>
132
+
133
+ That's not an ideal interface. It's a bit awkward to push a single event into an array for processing. We'll likely look to make this much better as we bring back batch processing of events.
134
+
@@ -72,6 +72,20 @@ module MingleEvents
72
72
  def to_s
73
73
  "Entry[entry_id=#{entry_id}, updated=#{updated}]"
74
74
  end
75
+
76
+ def eql?(object)
77
+ if object.equal?(self)
78
+ return true
79
+ elsif !self.class.equal?(object.class)
80
+ return false
81
+ end
82
+
83
+ return object.entry_id == entry_id
84
+ end
85
+
86
+ def ==(object)
87
+ eql?(object)
88
+ end
75
89
 
76
90
  private
77
91
 
@@ -24,7 +24,6 @@ WARNING!!
24
24
 
25
25
  # Fetch the content at location via HTTP. Throws error if non-200 response.
26
26
  def fetch_page(location)
27
- MingleEvents.log.info("About to fetch #{location}...")
28
27
  rsp = fetch_page_response(location)
29
28
  case rsp
30
29
  when Net::HTTPSuccess
@@ -51,18 +50,18 @@ See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_auth
51
50
  http.use_ssl = true
52
51
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
53
52
  else
54
- puts BASIC_AUTH_HTTP_WARNING
53
+ MingleEvents.log.warn BASIC_AUTH_HTTP_WARNING
55
54
  end
56
55
 
57
56
  path = uri.path
58
57
  path += "?#{uri.query}" if uri.query
59
- puts "Fetching page at #{path}..."
58
+ MingleEvents.log.info "Fetching page at #{path}..."
60
59
 
61
60
  start = Time.now
62
61
  req = Net::HTTP::Get.new(path)
63
62
  req.basic_auth(@username, @password)
64
63
  rsp = http.request(req)
65
- puts "...#{Time.now - start}"
64
+ MingleEvents.log.info "...#{path} fetched in #{Time.now - start} seconds."
66
65
 
67
66
  rsp
68
67
  end
@@ -24,11 +24,11 @@ module MingleEvents
24
24
 
25
25
  path = uri.path
26
26
  path += "?#{uri.query}" if uri.query
27
- puts "Fetching page at #{path}..."
27
+ MingleEvents.log.info "Fetching page at #{path}..."
28
28
 
29
29
  start = Time.now
30
30
  response = http.get(path, headers)
31
- puts "...#{Time.now - start}"
31
+ MingleEvents.log.info "... #{path} fetched in #{Time.now - start} seconds."
32
32
 
33
33
  # todo: what's the right way to raise on non 200 ?
34
34
  # raise StandardError.new(response.body) unless response == Net::HTTPSuccess
@@ -17,13 +17,9 @@ module MingleEvents
17
17
  @processors_by_project_identifier.each do |project_identifier, processors|
18
18
  fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access)
19
19
  fetcher.reset if options[:clean]
20
- info_file_for_new_event = fetcher.fetch_latest
21
- while info_file_for_new_event
22
- entry_info = YAML.load(File.new(info_file_for_new_event))
23
- entry = Feed::Entry.new(Nokogiri::XML(entry_info[:entry_xml]).at('/entry'))
20
+ fetcher.fetch_latest.each do |entry|
24
21
  MingleEvents.log.info("About to process event #{entry.entry_id}...")
25
22
  processors.each{|p| p.process_events([entry])}
26
- info_file_for_new_event = entry_info[:next_entry_file_path]
27
23
  end
28
24
  end
29
25
  end
@@ -10,22 +10,24 @@ module MingleEvents
10
10
  @state_dir = File.expand_path(state_dir || File.join('~', '.mingle_events', base_uri.host, base_uri.port.to_s, project_identifier, 'fetched_events'))
11
11
  end
12
12
 
13
+ # blow away any existing state, when next used to fetch events from mingle
14
+ # will crawl all the way back to time zero
13
15
  def reset
14
16
  FileUtils.rm_rf(@state_dir)
15
17
  end
16
18
 
19
+ # fetch the latest events from mingle, i.e., the ones not previously seen
17
20
  def fetch_latest
18
21
  page = Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
19
22
  most_recent_new_entry = page.entries.first
20
-
21
23
  last_fetched_entry = load_last_fetched_entry
22
- fetched_previously_seen_event = false
24
+ last_fetched_entry_seen = false
23
25
  next_entry = nil
24
- while !fetched_previously_seen_event && page
26
+ while !last_fetched_entry_seen && page
25
27
  page.entries.each do |entry|
26
28
 
27
29
  if last_fetched_entry && entry.entry_id == last_fetched_entry.entry_id
28
- fetched_previously_seen_event = true
30
+ last_fetched_entry_seen = true
29
31
  break
30
32
  end
31
33
 
@@ -35,12 +37,56 @@ module MingleEvents
35
37
  end
36
38
  page = page.next
37
39
  end
38
-
39
- update_current_state(most_recent_new_entry)
40
-
41
- file_for_entry(next_entry)
40
+
41
+ update_current_state(next_entry, most_recent_new_entry)
42
+ Entries.new(file_for_entry(next_entry), file_for_entry(most_recent_new_entry))
43
+ end
44
+
45
+ # returns all previously fetched entries; can be used to reprocess the events for, say,
46
+ # various historical analyses
47
+ def all_fetched_entries
48
+ current_state = load_current_state
49
+ Entries.new(current_state[:first_fetched_entry_info_file], current_state[:last_fetched_entry_info_file])
50
+ end
51
+
52
+ def first_entry_fetched
53
+ current_state_entry(:first_fetched_entry_info_file)
54
+ end
55
+
56
+ def last_entry_fetched
57
+ current_state_entry(:last_fetched_entry_info_file)
58
+ end
59
+
60
+ # only public to facilitate testing
61
+ def update_current_state(oldest_new_entry, most_recent_new_entry)
62
+ current_state = load_current_state
63
+ if most_recent_new_entry
64
+ current_state.merge!(:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry))
65
+ if current_state[:first_fetched_entry_info_file].nil?
66
+ current_state.merge!(:first_fetched_entry_info_file => file_for_entry(oldest_new_entry))
67
+ end
68
+ File.open(current_state_file, 'w'){|out| YAML.dump(current_state, out)}
69
+ end
70
+ end
71
+
72
+ # only public to facilitate testing
73
+ def write_entry_to_disk(entry, next_entry)
74
+ file = file_for_entry(entry)
75
+ FileUtils.mkdir_p(File.dirname(file))
76
+ file_content = {:entry_xml => entry.raw_xml, :next_entry_file_path => file_for_entry(next_entry)}
77
+ File.open(file, 'w'){|out| YAML.dump(file_content, out)}
42
78
  end
79
+
80
+ private
43
81
 
82
+ def current_state_entry(info_file_key)
83
+ if info_file = load_current_state[info_file_key]
84
+ entry_for_xml(YAML.load(File.new(info_file))[:entry_xml])
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
44
90
  def file_for_entry(entry)
45
91
  return nil if entry.nil?
46
92
 
@@ -55,15 +101,9 @@ module MingleEvents
55
101
  def current_state_file
56
102
  File.expand_path(File.join(@state_dir, 'current_state.yml'))
57
103
  end
58
-
59
- private
60
104
 
61
105
  def load_last_fetched_entry
62
- current_state = if File.exist?(current_state_file)
63
- YAML.load(File.new(current_state_file))
64
- else
65
- {:last_fetched_entry_info_file => nil}
66
- end
106
+ current_state = load_current_state
67
107
  last_fetched_entry = if current_state[:last_fetched_entry_info_file]
68
108
  last_fetched_entry_info = YAML.load(File.new(current_state[:last_fetched_entry_info_file]))
69
109
  Feed::Entry.new(Nokogiri::XML(last_fetched_entry_info[:entry_xml]).at('/entry'))
@@ -71,26 +111,43 @@ module MingleEvents
71
111
  nil
72
112
  end
73
113
  end
74
-
75
- def update_current_state(most_recent_new_entry)
76
- if most_recent_new_entry
77
- File.open(current_state_file, 'w') do |out|
78
- YAML.dump({:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry)}, out)
79
- end
80
- end
81
- end
82
114
 
83
- def write_entry_to_disk(entry, next_entry)
84
- file = file_for_entry(entry)
85
- FileUtils.mkdir_p(File.dirname(file))
86
- file_content = {
87
- :entry_xml => entry.raw_xml,
88
- :next_entry_file_path => file_for_entry(next_entry)
89
- }
90
- File.open(file, 'w') do |out|
91
- YAML.dump(file_content, out)
115
+ def load_current_state
116
+ if File.exist?(current_state_file)
117
+ YAML.load(File.new(current_state_file))
118
+ else
119
+ {:last_fetched_entry_info_file => nil, :first_fetched_entry_info_file => nil}
92
120
  end
93
121
  end
94
122
 
123
+ def entry_for_xml(entry_xml)
124
+ Feed::Entry.new(Nokogiri::XML(entry_xml).at('/entry'))
125
+ end
126
+
127
+ class Entries
128
+
129
+ include Enumerable
130
+
131
+ def initialize(first_info_file, last_info_file)
132
+ @first_info_file = first_info_file
133
+ @last_info_file = last_info_file
134
+ end
135
+
136
+ def entry_for_xml(entry_xml)
137
+ Feed::Entry.new(Nokogiri::XML(entry_xml).at('/entry'))
138
+ end
139
+
140
+ def each(&block)
141
+ current_file = @first_info_file
142
+ while current_file
143
+ current_entry_info = YAML.load(File.new(current_file))
144
+ yield(entry_for_xml(current_entry_info[:entry_xml]))
145
+ break if File.expand_path(current_file) == File.expand_path(@last_info_file)
146
+ current_file = current_entry_info[:next_entry_file_path]
147
+ end
148
+ end
149
+
150
+ end
151
+
95
152
  end
96
153
  end
@@ -134,6 +134,34 @@ module MingleEvents
134
134
  assert_equal(entry.entry_id, entry.event_id)
135
135
  end
136
136
 
137
+ def test_entry_id_determines_equality
138
+ element_xml_text_1 = %{
139
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
140
+ <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
141
+ <category term="page" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
142
+ </entry>}
143
+ entry_1 = Entry.new(Nokogiri::XML(element_xml_text_1))
144
+
145
+ element_xml_text_2 = %{
146
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
147
+ <id>https://mingle.example.com/projects/mingle/events/index/234443</id>
148
+ <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
149
+ </entry>}
150
+ entry_2 = Entry.new(Nokogiri::XML(element_xml_text_2))
151
+
152
+ element_xml_text_3 = %{
153
+ <entry xmlns:mingle="http://www.thoughtworks-studios.com/ns/mingle">
154
+ <id>https://mingle.example.com/projects/mingle/events/index/234</id>
155
+ <category term="card" scheme="http://www.thoughtworks-studios.com/ns/mingle#categories"/>
156
+ </entry>}
157
+ entry_3 = Entry.new(Nokogiri::XML(element_xml_text_3))
158
+
159
+ assert entry_1.eql?(entry_2)
160
+ assert entry_1 == entry_2
161
+ assert !entry_2.eql?(entry_3)
162
+ assert entry_2 != entry_3
163
+ end
164
+
137
165
  end
138
166
 
139
167
  end
@@ -7,60 +7,34 @@ module MingleEvents
7
7
  state_dir = temp_dir
8
8
  fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
9
9
 
10
- file_for_first_new_entry = fetcher.fetch_latest
11
-
12
- expected_entry_ids = [
13
- 'https://mingle.example.com/projects/atlas/events/index/23',
14
- 'https://mingle.example.com/projects/atlas/events/index/97',
15
- 'https://mingle.example.com/projects/atlas/events/index/98',
16
- 'https://mingle.example.com/projects/atlas/events/index/99',
17
- 'https://mingle.example.com/projects/atlas/events/index/100',
18
- 'https://mingle.example.com/projects/atlas/events/index/101',
19
- 'https://mingle.example.com/projects/atlas/events/index/103'
20
- ]
21
-
22
- assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
10
+ latest_entries = fetcher.fetch_latest
11
+ expected_latest_entries = [23, 97, 98, 99, 100, 101, 103].map{|n| entry(n)}
12
+ assert_equal(expected_latest_entries, latest_entries.to_a)
13
+ assert_equal entry(23), fetcher.first_entry_fetched
14
+ assert_equal entry(103), fetcher.last_entry_fetched
23
15
  end
24
16
 
25
17
  def test_can_fetch_all_entries_and_write_to_disk_when_existing_state
26
18
  state_dir = temp_dir
27
19
  fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
28
- setup_current_state(%{
29
- <entry>
30
- <id>https://mingle.example.com/projects/atlas/events/index/97</id>
31
- <title>entry 97</title>
32
- <updated>2011-02-03T01:10:52Z</updated>
33
- <author><name>Harry</name></author>
34
- </entry>}, fetcher)
35
20
 
36
- file_for_first_new_entry = fetcher.fetch_latest
21
+ setup_current_state(23, 97, 97, fetcher)
37
22
 
38
- expected_entry_ids = [
39
- 'https://mingle.example.com/projects/atlas/events/index/98',
40
- 'https://mingle.example.com/projects/atlas/events/index/99',
41
- 'https://mingle.example.com/projects/atlas/events/index/100',
42
- 'https://mingle.example.com/projects/atlas/events/index/101',
43
- 'https://mingle.example.com/projects/atlas/events/index/103'
44
- ]
45
-
46
- assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
23
+ latest_entries = fetcher.fetch_latest
24
+ expected_latest_entries = [98, 99, 100, 101, 103].map{|n| entry(n)}
25
+ assert_equal(expected_latest_entries, latest_entries.to_a)
26
+ assert_equal entry(23), fetcher.first_entry_fetched
27
+ assert_equal entry(103), fetcher.last_entry_fetched
47
28
  end
48
29
 
49
30
  def test_no_new_entries_with_current_state
50
31
  state_dir = temp_dir
51
32
  fetcher = ProjectEventFetcher.new('atlas', stub_mingle_access, state_dir)
52
- setup_current_state(%{
53
- <entry>
54
- <id>https://mingle.example.com/projects/atlas/events/index/103</id>
55
- <title>entry 103</title>
56
- <updated>2011-02-03T01:10:52Z</updated>
57
- <author><name>Harry</name></author>
58
- </entry>}, fetcher)
33
+ setup_current_state(23, 97, 103, fetcher)
59
34
 
60
- assert_nil fetcher.fetch_latest
61
-
62
- current_state = YAML.load(File.new(File.join(state_dir, 'current_state.yml')))
63
- assert_equal(@last_fetched_entry_info_file, current_state[:last_fetched_entry_info_file])
35
+ assert fetcher.fetch_latest.to_a.empty?
36
+ assert_equal entry(23), fetcher.first_entry_fetched
37
+ assert_equal entry(103), fetcher.last_entry_fetched
64
38
  end
65
39
 
66
40
  def test_no_new_entries_with_no_current_state
@@ -77,46 +51,32 @@ module MingleEvents
77
51
  </feed>})
78
52
  fetcher = ProjectEventFetcher.new('atlas', mingle_access, state_dir)
79
53
 
80
- assert_nil fetcher.fetch_latest
81
- assert !File.exist?(File.join(state_dir, 'current_state.yml'))
54
+ assert fetcher.fetch_latest.to_a.empty?
55
+ assert_nil fetcher.first_entry_fetched
56
+ assert_nil fetcher.last_entry_fetched
82
57
  end
83
58
 
84
59
  private
85
60
 
86
- def setup_current_state(last_fetched_entry_xml, fetcher)
87
- last_fetched_entry_info = {
88
- :entry_xml => last_fetched_entry_xml,
89
- :next_entry_file_path => nil
90
- }
91
- @last_fetched_entry_info_file = fetcher.file_for_entry(Feed::Entry.new(Nokogiri::XML(last_fetched_entry_xml).at('/entry')))
92
- FileUtils.mkdir_p(File.dirname(@last_fetched_entry_info_file))
93
- File.open(@last_fetched_entry_info_file, 'w') do |out|
94
- YAML.dump(last_fetched_entry_info, out)
95
- end
96
- current_state = {:last_fetched_entry_info_file => @last_fetched_entry_info_file}
97
- File.open(fetcher.current_state_file, 'w') do |out|
98
- YAML.dump(current_state, out)
99
- end
100
-
61
+ def setup_current_state(first_entry_id, second_entry_id, last_entry_id, fetcher)
62
+ first_entry = entry(first_entry_id)
63
+ second_entry = entry(second_entry_id)
64
+ last_entry = entry(last_entry_id)
65
+ fetcher.write_entry_to_disk(first_entry, second_entry)
66
+ fetcher.write_entry_to_disk(last_entry, nil)
67
+ fetcher.update_current_state(first_entry, last_entry)
101
68
  end
102
-
103
- def assert_expected_entry_chain_written_to_disk(expected_entry_ids, file_for_first_new_entry, fetcher)
104
-
105
- file_for_new_entry = file_for_first_new_entry
106
- file_for_last_inspected_entry = nil
107
- expected_entry_ids.each_with_index do |expected_entry_id, index|
108
- file_for_last_inspected_entry = file_for_new_entry
109
- new_entry_info = YAML.load(File.new(file_for_new_entry))
110
- new_entry = Feed::Entry.new(Nokogiri::XML(new_entry_info[:entry_xml]).at('/entry'))
111
- assert_equal(expected_entry_id, new_entry.entry_id)
112
- file_for_new_entry = new_entry_info[:next_entry_file_path]
113
- end
114
-
115
- current_state = YAML.load(File.new(fetcher.current_state_file))
116
- assert_equal(file_for_last_inspected_entry, current_state[:last_fetched_entry_info_file])
117
-
69
+
70
+ def entry(entry_id)
71
+ entry_xml = %{
72
+ <entry>
73
+ <id>https://mingle.example.com/projects/atlas/events/index/#{entry_id}</id>
74
+ <title>entry #{entry_id}</title>
75
+ <updated>2011-02-03T01:10:52Z</updated>
76
+ <author><name>Bob</name></author>
77
+ </entry>
78
+ }
79
+ Feed::Entry.new(Nokogiri::XML(entry_xml).at('/entry'))
118
80
  end
119
-
120
-
121
81
  end
122
82
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mingle_events
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 4
10
- version: 0.0.4
9
+ - 6
10
+ version: 0.0.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Rice