wukong 0.1.4 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/INSTALL.textile +89 -0
- data/README.textile +41 -74
- data/docpages/INSTALL.textile +94 -0
- data/{doc → docpages}/LICENSE.textile +0 -0
- data/{doc → docpages}/README-wulign.textile +6 -0
- data/docpages/UsingWukong-part1-get_ready.textile +17 -0
- data/{doc/overview.textile → docpages/UsingWukong-part2-ThinkingBigData.textile} +8 -24
- data/{doc → docpages}/UsingWukong-part3-parsing.textile +8 -2
- data/docpages/_config.yml +39 -0
- data/{doc/tips.textile → docpages/bigdata-tips.textile} +71 -44
- data/{doc → docpages}/code/api_response_example.txt +0 -0
- data/{doc → docpages}/code/parser_skeleton.rb +0 -0
- data/{doc/intro_to_map_reduce → docpages/diagrams}/MapReduceDiagram.graffle +0 -0
- data/docpages/favicon.ico +0 -0
- data/docpages/gem.css +16 -0
- data/docpages/hadoop-tips.textile +83 -0
- data/docpages/index.textile +90 -0
- data/docpages/intro.textile +8 -0
- data/docpages/moreinfo.textile +174 -0
- data/docpages/news.html +24 -0
- data/{doc → docpages}/pig/PigLatinExpressionsList.txt +0 -0
- data/{doc → docpages}/pig/PigLatinReferenceManual.html +0 -0
- data/{doc → docpages}/pig/PigLatinReferenceManual.txt +0 -0
- data/docpages/tutorial.textile +283 -0
- data/docpages/usage.textile +195 -0
- data/docpages/wutils.textile +263 -0
- data/wukong.gemspec +80 -50
- metadata +87 -54
- data/doc/INSTALL.textile +0 -41
- data/doc/README-tutorial.textile +0 -163
- data/doc/README-wutils.textile +0 -128
- data/doc/TODO.textile +0 -61
- data/doc/UsingWukong-part1-setup.textile +0 -2
- data/doc/UsingWukong-part2-scraping.textile +0 -2
- data/doc/hadoop-nfs.textile +0 -51
- data/doc/hadoop-setup.textile +0 -29
- data/doc/index.textile +0 -124
- data/doc/links.textile +0 -42
- data/doc/usage.textile +0 -102
- data/doc/utils.textile +0 -48
- data/examples/and_pig/sample_queries.rb +0 -128
- data/lib/wukong/and_pig.rb +0 -62
- data/lib/wukong/and_pig/README.textile +0 -12
- data/lib/wukong/and_pig/as.rb +0 -37
- data/lib/wukong/and_pig/data_types.rb +0 -30
- data/lib/wukong/and_pig/functions.rb +0 -50
- data/lib/wukong/and_pig/generate.rb +0 -85
- data/lib/wukong/and_pig/generate/variable_inflections.rb +0 -82
- data/lib/wukong/and_pig/junk.rb +0 -51
- data/lib/wukong/and_pig/operators.rb +0 -8
- data/lib/wukong/and_pig/operators/compound.rb +0 -29
- data/lib/wukong/and_pig/operators/evaluators.rb +0 -7
- data/lib/wukong/and_pig/operators/execution.rb +0 -15
- data/lib/wukong/and_pig/operators/file_methods.rb +0 -29
- data/lib/wukong/and_pig/operators/foreach.rb +0 -98
- data/lib/wukong/and_pig/operators/groupies.rb +0 -212
- data/lib/wukong/and_pig/operators/load_store.rb +0 -65
- data/lib/wukong/and_pig/operators/meta.rb +0 -42
- data/lib/wukong/and_pig/operators/relational.rb +0 -129
- data/lib/wukong/and_pig/pig_struct.rb +0 -48
- data/lib/wukong/and_pig/pig_var.rb +0 -95
- data/lib/wukong/and_pig/symbol.rb +0 -29
- data/lib/wukong/and_pig/utils.rb +0 -0
data/docpages/news.html
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: edamame news
|
4
|
+
collapse: true
|
5
|
+
---
|
6
|
+
<h1 class="gemheader">{% if site.gemname %}{{ site.gemname }}{% else %}mrflip{% endif %}<span class="small">:: news</span></h1>
|
7
|
+
|
8
|
+
<div id="news">
|
9
|
+
{% for t in site.posts %} {% assign has_posts = true %}{% endfor %}{% if has_posts %}
|
10
|
+
{% for post in site.posts %}
|
11
|
+
<div class="toggle" id="news-{{ post.id }}">
|
12
|
+
|
13
|
+
<h2><a href="{{ post.url }}">{{ post.title }}</a><span class="postdate"> » {{ post.date | date_to_string }}</span></h2>
|
14
|
+
|
15
|
+
{{ post.content }}
|
16
|
+
|
17
|
+
</div>
|
18
|
+
{% endfor %}
|
19
|
+
{% else %}
|
20
|
+
<p class="heavy">
|
21
|
+
<em>(no news. good news?)</em>
|
22
|
+
</p>
|
23
|
+
{% endif %}
|
24
|
+
</div>
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,283 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: mrflip.github.com/wukong - Tutorial
|
4
|
+
collapse: false
|
5
|
+
---
|
6
|
+
|
7
|
+
h1(gemheader). Tutorial by Examples
|
8
|
+
|
9
|
+
|
10
|
+
<notextile><div class="toggle"></notextile>
|
11
|
+
|
12
|
+
h2(#wordcount). Count Words
|
13
|
+
|
14
|
+
Here's a script to count words in a text stream:
|
15
|
+
|
16
|
+
{% highlight ruby %}
|
17
|
+
require 'wukong'
|
18
|
+
module WordCount
|
19
|
+
class Mapper < Wukong::Streamer::LineStreamer
|
20
|
+
# Emit each word in the line.
|
21
|
+
def process line
|
22
|
+
words = line.strip.split(/\W+/).reject(&:blank?)
|
23
|
+
words.each{|word| yield [word, 1] }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Reducer < Wukong::Streamer::ListReducer
|
28
|
+
def finalize
|
29
|
+
yield [ key, values.map(&:last).map(&:to_i).sum ]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Wukong::Script.new(
|
35
|
+
WordCount::Mapper,
|
36
|
+
WordCount::Reducer
|
37
|
+
).run # Execute the script
|
38
|
+
{% endhighlight %}
|
39
|
+
|
40
|
+
The first class, the Mapper, eats lines and craps @[word, count]@ records. Here
|
41
|
+
the /key/ is the word, and the /value/ is its count.
|
42
|
+
|
43
|
+
The second class is an example of an accumulated list reducer. The values for
|
44
|
+
each key are stacked up into a list; then the record(s) yielded by @#finalize@
|
45
|
+
are emitted.
|
46
|
+
|
47
|
+
Here's another way to write the Reducer: accumulate the count of each line, then
|
48
|
+
yield the sum in @#finalize@:
|
49
|
+
|
50
|
+
{% highlight ruby %}
|
51
|
+
class Reducer2 < Wukong::Streamer::AccumulatingReducer
|
52
|
+
attr_accessor :key_count
|
53
|
+
def start! *args
|
54
|
+
self.key_count = 0
|
55
|
+
end
|
56
|
+
def accumulate(word, count)
|
57
|
+
self.key_count += count.to_i
|
58
|
+
end
|
59
|
+
def finalize
|
60
|
+
yield [ key, key_count ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
{% endhighlight %}
|
64
|
+
|
65
|
+
Of course you can be really lazy (i.e. smart) and write your script as
|
66
|
+
|
67
|
+
{% highlight ruby %}
|
68
|
+
class Script < Wukong::Script
|
69
|
+
def reducer_command
|
70
|
+
'uniq -c'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
{% endhighlight %}
|
74
|
+
|
75
|
+
h2(#structstream). Structured data
|
76
|
+
|
77
|
+
The previous example dealt with unstructured data. Wukong also lets you view your data as a stream of structured objects.
|
78
|
+
|
79
|
+
Let's say you have a blog; its records look like
|
80
|
+
|
81
|
+
{% highlight ruby %}
|
82
|
+
Post = Struct.new( :id, :created_at, :user_id, :title, :body, :link )
|
83
|
+
Comment = Struct.new( :id, :created_at, :post_id, :user_id, :body )
|
84
|
+
User = Struct.new( :id, :username, :fullname, :homepage, :description )
|
85
|
+
UserLoc = Struct.new( :user_id, :text, :lat, :lng )
|
86
|
+
{% endhighlight %}
|
87
|
+
|
88
|
+
You've been using "twitter":http://twitter.com for a long time, and you've written something that from now on will inject all your tweets as Posts, and all replies to them as Comments (by a common 'twitter_bot' account on your blog).What about the past two years' worth of tweets? Let's assume you're so chatty that a Map/Reduce script is warranted to handle the volume. (Actually, wukong makes a really nice ETL package, so this may be convienient even at small scale).
|
89
|
+
|
90
|
+
Cook up something that scrapes your tweets and all replies to your tweets:
|
91
|
+
|
92
|
+
{% highlight ruby %}
|
93
|
+
Tweet = Struct.new( :id, :created_at, :twitter_user_id,
|
94
|
+
:in_reply_to_user_id, :in_reply_to_status_id, :text )
|
95
|
+
TwitterUser = Struct.new( :id, :username, :fullname,
|
96
|
+
:homepage, :location, :description )
|
97
|
+
{% endhighlight %}
|
98
|
+
|
99
|
+
Now we'll just process all those in a big pile, converting to Posts, Comments and Users as appropriate. Serialize your scrape results so that each Tweet and each TwitterUser is a single lines containing first the class name ('tweet' or 'twitter_user') followed by its constituent fields, in order, separated by tabs.
|
100
|
+
|
101
|
+
The RecordStreamer takes each such line, constructs its corresponding class, and instantiates it with the
|
102
|
+
|
103
|
+
{% highlight ruby %}
|
104
|
+
require 'wukong'
|
105
|
+
require 'my_blog' #defines the blog models
|
106
|
+
module TwitBlog
|
107
|
+
class Mapper < Wukong::Streamer::RecordStreamer
|
108
|
+
# Watch for tweets by me
|
109
|
+
MY_USER_ID = 24601
|
110
|
+
# structs for our input objects
|
111
|
+
Tweet = Struct.new( :id, :created_at, :twitter_user_id,
|
112
|
+
:in_reply_to_user_id, :in_reply_to_status_id, :text )
|
113
|
+
TwitterUser = Struct.new( :id, :username, :fullname,
|
114
|
+
:homepage, :location, :description )
|
115
|
+
#
|
116
|
+
# If this is a tweet is by me, convert it to a Post.
|
117
|
+
#
|
118
|
+
# If it is a tweet not by me, convert it to a Comment that
|
119
|
+
# will be paired with the correct Post.
|
120
|
+
#
|
121
|
+
# If it is a TwitterUser, convert it to a User record and
|
122
|
+
# a user_location record
|
123
|
+
#
|
124
|
+
def process record
|
125
|
+
case record
|
126
|
+
when TwitterUser
|
127
|
+
user = MyBlog::User.new.merge(record) # grab the fields in common
|
128
|
+
user_loc = MyBlog::UserLoc.new(record.id, record.location, nil, nil)
|
129
|
+
yield user
|
130
|
+
yield user_loc
|
131
|
+
when Tweet
|
132
|
+
if record.twitter_user_id == MY_USER_ID
|
133
|
+
post = MyBlog::Post.new.merge record
|
134
|
+
post.link = "http://twitter.com/statuses/show/#{record.id}"
|
135
|
+
post.body = record.text
|
136
|
+
post.title = record.text[0..65] + "..."
|
137
|
+
yield post
|
138
|
+
else
|
139
|
+
comment = MyBlog::Comment.new.merge record
|
140
|
+
comment.body = record.text
|
141
|
+
comment.post_id = record.in_reply_to_status_id
|
142
|
+
yield comment
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
Wukong::Script.new( TwitBlog::Mapper, nil ).run # identity reducer
|
149
|
+
{% endhighlight %}
|
150
|
+
|
151
|
+
h2(#accumulators). Accumulators
|
152
|
+
|
153
|
+
h3(#uniqifying). A Uniqifying Accumulator
|
154
|
+
|
155
|
+
|
156
|
+
The script above uses the identity reducer: every record from the mapper is sent
|
157
|
+
to the output. But what if you had grabbed the replying user's record every time you saw a reply?
|
158
|
+
|
159
|
+
You'd like to just pass it through @uniq@. But if something has changed in the interim, or if you record a timestamp for each sample, you won't be able to use the simple @uniq@ command. You'd like to just get one example for each key!
|
160
|
+
|
161
|
+
Wukong includes just such a reducer, the UniqByLastReducer:
|
162
|
+
|
163
|
+
{% highlight ruby %}
|
164
|
+
#
|
165
|
+
# UniqByLastReducer accepts all records for a given key and emits only the
|
166
|
+
# last-seen.
|
167
|
+
#
|
168
|
+
# It acts like an insecure high-school kid: for each record of a given key
|
169
|
+
# it discards whatever record it's holding and adopts this new value. When a
|
170
|
+
# new key comes on the scene it emits the last record, like an older brother
|
171
|
+
# handing off his Depeche Mode collection.
|
172
|
+
#
|
173
|
+
# For example, to extract the *latest* value for each property, emit your
|
174
|
+
# records as
|
175
|
+
#
|
176
|
+
# [resource_type, key, timestamp, ... fields ...]
|
177
|
+
#
|
178
|
+
# then set :sort_fields to 3 and :partition_fields to 2.
|
179
|
+
#
|
180
|
+
class UniqByLastReducer < Wukong::Streamer::AccumulatingReducer
|
181
|
+
attr_accessor :final_value
|
182
|
+
|
183
|
+
#
|
184
|
+
# Use first two fields as keys by default
|
185
|
+
#
|
186
|
+
def get_key *vals
|
187
|
+
vals[0..1]
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# Adopt each value in turn: the last one's the one you want.
|
192
|
+
#
|
193
|
+
def accumulate *vals
|
194
|
+
self.final_value = vals
|
195
|
+
end
|
196
|
+
|
197
|
+
#
|
198
|
+
# Emit the last-seen value
|
199
|
+
#
|
200
|
+
def finalize
|
201
|
+
yield final_value if final_value
|
202
|
+
end
|
203
|
+
|
204
|
+
#
|
205
|
+
# Clear state on reset
|
206
|
+
#
|
207
|
+
def start! *args
|
208
|
+
self.final_value = nil
|
209
|
+
end
|
210
|
+
end
|
211
|
+
{% endhighlight %}
|
212
|
+
|
213
|
+
h3(#groupby). A GroupBy Accumulator
|
214
|
+
|
215
|
+
Wukong has a good collection of map/reduce patterns. For example, it's quite common to accumulate all records for a given key and emit some result based on the whole group. The
|
216
|
+
|
217
|
+
The AccumulatingReducer calls start! on the first record for each key, calls accumulate() on every example for that key (including the first), and calls finalize() once the last record for that key is seen.
|
218
|
+
|
219
|
+
Here's an AccumulatingReducer that takes a long list of key-value pairs and emits, for each key, all its corresponding values in one line.
|
220
|
+
|
221
|
+
{% highlight ruby %}
|
222
|
+
#
|
223
|
+
# Roll up all values for each key into a single line
|
224
|
+
#
|
225
|
+
class GroupByReducer < Wukong::Streamer::AccumulatingReducer
|
226
|
+
attr_accessor :values
|
227
|
+
|
228
|
+
# Start with an empty list
|
229
|
+
def start! *args
|
230
|
+
self.values = []
|
231
|
+
end
|
232
|
+
|
233
|
+
# Aggregate each value in turn
|
234
|
+
def accumulate key, value
|
235
|
+
self.values << value
|
236
|
+
end
|
237
|
+
|
238
|
+
# Emit the key and all values, tab-separated
|
239
|
+
def finalize
|
240
|
+
yield [key, values].flatten
|
241
|
+
end
|
242
|
+
end
|
243
|
+
{% endhighlight %}
|
244
|
+
|
245
|
+
So given adjacency pairs for the following directed friend graph:
|
246
|
+
|
247
|
+
<pre>
|
248
|
+
@jerry @elaine
|
249
|
+
@elaine @jerry
|
250
|
+
@jerry @kramer
|
251
|
+
@kramer @jerry
|
252
|
+
@kramer @bobsacamato
|
253
|
+
@kramer @newman
|
254
|
+
@jerry @superman
|
255
|
+
@newman @kramer
|
256
|
+
@newman @elaine
|
257
|
+
@newman @jerry
|
258
|
+
</pre>
|
259
|
+
|
260
|
+
You'd end up with
|
261
|
+
|
262
|
+
<pre> @elaine @jerry
|
263
|
+
@jerry @elaine @kramer @superman
|
264
|
+
@kramer @bobsacamato @jerry @newman
|
265
|
+
@newman @elaine @jerry @kramer
|
266
|
+
</pre>
|
267
|
+
|
268
|
+
|
269
|
+
h2. A note about keys
|
270
|
+
|
271
|
+
Now we're going to write this using the synthetic keys already extant in the
|
272
|
+
twitter records, making the unwarranted assumption that they won't collide with
|
273
|
+
the keys in your database.
|
274
|
+
|
275
|
+
Map/Reduce paradigm does badly with synthetic keys. Synthetic keys demand
|
276
|
+
locality, and map/reduce's remarkable scaling comes from not assuming
|
277
|
+
locality. In general, write your map/reduce scripts to use natural keys (the scre
|
278
|
+
|
279
|
+
h2. More...
|
280
|
+
|
281
|
+
There are many useful examples (including an actually-useful version of this
|
282
|
+
WordCount script) in the "examples/ directory.":http://github.com/mrflip/wukong/tree/master/examples
|
283
|
+
|
@@ -0,0 +1,195 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Usage notes
|
4
|
+
---
|
5
|
+
|
6
|
+
h1(gemheader). {{ site.gemname }} %(small):: usage%
|
7
|
+
|
8
|
+
** "How to run a Wukong script":#running
|
9
|
+
** "How to test your scripts":#testing
|
10
|
+
** "Wukong Plays nicely with others":#playnice
|
11
|
+
** "Schema export":#schema_export to Pig or SQL
|
12
|
+
** "Wukong's internal workflow":#workflow
|
13
|
+
** "Using wukong with internal streaming":#stayinruby
|
14
|
+
** "Using wukong to Batch-Process ActiveRecord Objects":#activerecord
|
15
|
+
|
16
|
+
|
17
|
+
<notextile><div class="toggle"></notextile>
|
18
|
+
|
19
|
+
h2(#running). How to run a Wukong script
|
20
|
+
|
21
|
+
To run your script using local files and no connection to a hadoop cluster,
|
22
|
+
|
23
|
+
pre. your/script.rb --run=local path/to/input_files path/to/output_dir
|
24
|
+
|
25
|
+
To run the command across a Hadoop cluster,
|
26
|
+
|
27
|
+
pre. your/script.rb --run=hadoop path/to/input_files path/to/output_dir
|
28
|
+
|
29
|
+
You can set the default in the config/wukong-site.yaml file, and then just use @--run@ instead of @--run=something@ --it will just use the default run mode.
|
30
|
+
|
31
|
+
If you're running @--run=hadoop@, all file paths are HDFS paths. If you're running @--run=local@, all file paths are local paths. (your/script path, of course, lives on the local filesystem).
|
32
|
+
|
33
|
+
You can supply arbitrary command line arguments (they wind up as key-value pairs in the options path your mapper and reducer receive), and you can use the hadoop syntax to specify more than one input file:
|
34
|
+
|
35
|
+
pre. ./path/to/your/script.rb --any_specific_options --options=can_have_vals \
|
36
|
+
--run "input_dir/part_*,input_file2.tsv,etc.tsv" path/to/output_dir
|
37
|
+
|
38
|
+
Note that all @--options@ must precede (in any order) all non-options.
|
39
|
+
|
40
|
+
<notextile></div><div class="toggle"></notextile>
|
41
|
+
|
42
|
+
h2(#testing). How to test your scripts
|
43
|
+
|
44
|
+
To run mapper on its own:
|
45
|
+
|
46
|
+
pre. cat ./local/test/input.tsv | ./examples/word_count.rb --map | more
|
47
|
+
|
48
|
+
or if your test data lies on the HDFS,
|
49
|
+
|
50
|
+
pre. hdp-cat test/input.tsv | ./examples/word_count.rb --map | more
|
51
|
+
|
52
|
+
Next graduate to running @--run=local@ mode so you can inspect the reducer.
|
53
|
+
|
54
|
+
<notextile></div><div class="toggle"></notextile>
|
55
|
+
|
56
|
+
h2(#playnice). Wukong Plays nicely with others
|
57
|
+
|
58
|
+
Wukong is friends with "Hadoop":http://hadoop.apache.org/core the elephant, "Pig":http://hadoop.apache.org/pig/ the query language, and the @cat@ on your command line. It even has limited support for "martinis":http://datamapper.org (Datamapper) and "express trains":http://wiki.rubyonrails.org/rails/pages/ActiveRecord (ActiveRecord).
|
59
|
+
|
60
|
+
* "Export Wukong classes to SQL or Pig":#schema_export -- easily bulk-load and define SQL tables, or kickstart your pig scripts
|
61
|
+
* "Batch-Process records from ActiveRecord":#activerecord (the datamapper case is similar)
|
62
|
+
* Cascade Mappers and Reducers "purely in ruby":#stayinruby -- reportedly useful in an "ETL":http://en.wikipedia.org/wiki/Extract,_transform,_load context.
|
63
|
+
|
64
|
+
h3(#schema_export). Schema export to Pig or SQL
|
65
|
+
|
66
|
+
There is preliminary support for dumping wukong classes as schemata for other tools. For example, given the following:
|
67
|
+
|
68
|
+
{% highlight ruby %}
|
69
|
+
require "wukong" ;
|
70
|
+
require "wukong/schema"
|
71
|
+
User = TypedStruct.new(
|
72
|
+
[:id, Integer],
|
73
|
+
[:scraped_at, Bignum],
|
74
|
+
[:screen_name, String],
|
75
|
+
[:followers_count, Integer],
|
76
|
+
[:created_at, Bignum]
|
77
|
+
);
|
78
|
+
{% endhighlight %}
|
79
|
+
|
80
|
+
You can make a snippet for loading into pig with @puts User.load_pig@:
|
81
|
+
|
82
|
+
<pre> LOAD users.tsv AS ( rsrc:chararray, id: int, scraped_at: long, screen_name: chararray, followers_count: int, created_at: long )</pre>
|
83
|
+
|
84
|
+
Export to SQL with @puts User.sql_create_table ; puts User.sql_load_mysql@:
|
85
|
+
|
86
|
+
{% highlight sql %}
|
87
|
+
CREATE TABLE `users` (
|
88
|
+
`id` INT,
|
89
|
+
`scraped_at` BIGINT,
|
90
|
+
`screen_name` VARCHAR(255) CHARACTER SET ASCII,
|
91
|
+
`followers_count` INT,
|
92
|
+
`created_at` BIGINT
|
93
|
+
) ;
|
94
|
+
ALTER TABLE `user` DISABLE KEYS;
|
95
|
+
LOAD DATA LOCAL INFILE 'user.tsv'
|
96
|
+
REPLACE INTO TABLE `user`
|
97
|
+
COLUMNS
|
98
|
+
TERMINATED BY '\t'
|
99
|
+
OPTIONALLY ENCLOSED BY ''
|
100
|
+
ESCAPED BY ''
|
101
|
+
LINES STARTING BY 'user'
|
102
|
+
( @dummy,
|
103
|
+
`id`, `scraped_at`, `screen_name`, `followers_count`, `created_at`
|
104
|
+
);
|
105
|
+
ALTER TABLE `user` ENABLE KEYS ;
|
106
|
+
SELECT 'user', NOW(), COUNT(*) FROM `user`;
|
107
|
+
{% endhighlight %}
|
108
|
+
|
109
|
+
<notextile></div><div class="toggle"></notextile>
|
110
|
+
|
111
|
+
h2(#workflow). Wukong's internal workflow
|
112
|
+
|
113
|
+
Here's a somewhat detailed overview of a wukong script's internal workflow.
|
114
|
+
|
115
|
+
# You call @./myscript.rb --run infile outfile@
|
116
|
+
# Execution begins in the run method of the Script class (@wukong/script.rb@). It launches (depending on if you're local or remote) one of
|
117
|
+
** @cat infile | ./myscript.rb --map | sort | ./myscript.rb --reduce > outfile@
|
118
|
+
** @hadoop [a_crapton_of_streaming_args] -mapper './myscript.rb --map' -reducer './myscript.rb --reduce' @
|
119
|
+
# In either case, the effect is to spawn the exact same script you ran at the command line: one or more times with the --map command in place of the --run command, and one or more times with the --reduce command in place of the --run command. %(quiet)(well, unless you specify no reducers or a :map_command or something)%
|
120
|
+
|
121
|
+
# With the @--map@ or @--reduce@ flag given, the Script flag turns over control to the corresponding class: either @mapper_klass.new(self.options).stream@ or @reducer_klass.new(self.options).stream@
|
122
|
+
|
123
|
+
When in @--map@ or @--reduce@ mode (we'll just use @--map@ as an example):
|
124
|
+
|
125
|
+
# The mapper_klass is usually a subclass of @Streamer::Base@, but in actual fact it can be anything that initializes from a hash of options and responds to #stream.
|
126
|
+
# The default #stream method
|
127
|
+
** calls the before_stream hook
|
128
|
+
** reads each line from stdin ; #recordizes it ; passes it (if non-nil) to #process ; and emits each object yielded by #process
|
129
|
+
** calls its after_stream hook
|
130
|
+
# You typically leave #stream alone and just override #process.
|
131
|
+
# The accumulator classes build on these patterns (they're proper subclasses of Streamer::Base), but are used differently. With an accumulator, you should implement some or all of
|
132
|
+
** #start! -- called at the start of each accumulation, passing in the first record for that key
|
133
|
+
** #accumulate -- called on each record (including that first one)
|
134
|
+
** #finalize -- called when the last key of this accumulation is seen.
|
135
|
+
** #get_key -- called on each record to recover its key.
|
136
|
+
|
137
|
+
|
138
|
+
h3(#stayinruby). Using wukong with internal streaming
|
139
|
+
|
140
|
+
If you're using wukong in local mode, you may not want to spawn new processes all over the place. Or your records may arrive not from the command line but from, say, a database call.
|
141
|
+
|
142
|
+
In that case, just override #stream. The original:
|
143
|
+
|
144
|
+
{% highlight ruby %}
|
145
|
+
#
|
146
|
+
# Pass each record to +#process+
|
147
|
+
#
|
148
|
+
def stream
|
149
|
+
before_stream
|
150
|
+
$stdin.each do |line|
|
151
|
+
record = recordize(line.chomp)
|
152
|
+
next unless record
|
153
|
+
process(*record) do |output_record|
|
154
|
+
emit output_record
|
155
|
+
end
|
156
|
+
end
|
157
|
+
after_stream
|
158
|
+
end
|
159
|
+
{% endhighlight %}
|
160
|
+
|
161
|
+
h3(#activerecord). Using wukong to Batch-Process ActiveRecord Objects
|
162
|
+
|
163
|
+
Here's a stream method, overridden to batch-process ActiveRecord objects (untested sample code):
|
164
|
+
|
165
|
+
{% highlight ruby %}
|
166
|
+
class Mapper < Wukong::Streamer
|
167
|
+
# Set record_klass to the ActiveRecord class you'd like to batch process
|
168
|
+
cattr_accessor :record_klass
|
169
|
+
# Size of each batch to pull from the database
|
170
|
+
cattr_accessor :batch_size
|
171
|
+
|
172
|
+
#
|
173
|
+
# Grab records from the database in batches,
|
174
|
+
# pass each record to +#process+
|
175
|
+
#
|
176
|
+
# Everything downstream of this is agnostic of the fact that
|
177
|
+
# records are coming from the database and not $stdin
|
178
|
+
#
|
179
|
+
def stream
|
180
|
+
before_stream
|
181
|
+
record_klass.find_in_batches(:batch_size => batch_size ) do |record_batch|
|
182
|
+
record_batch.each do |record|
|
183
|
+
process(record.id, record) do |output_record|
|
184
|
+
emit output_record
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
after_stream
|
189
|
+
end
|
190
|
+
|
191
|
+
# ....
|
192
|
+
end
|
193
|
+
{% endhighlight %}
|
194
|
+
|
195
|
+
<notextile></div></notextile>
|