Parkour namespaces guide
August 17, 2014 · View on GitHub
Parkour’s namespaces fall into a handful of different categories, each described below.
Basic Hadoop integration
Parkour’s basic Hadoop integration namespaces provide convenient Clojure access to the core Hadoop APIs necessary for writing MapReduce jobs.
parkour.conf
The parkour.conf namespace contains functions for manipulating Hadoop
Configuration objects as bash-in-place mutable maps. It provides type-aware
string conversion on parameter-set, type-specific functions for parameter
access, and automatic conversion for the wrapped Configurations of objects
such as Jobs and Configurables. It also provides a dynamically-scoped base
default Configuration, and a tagged literal form for Configurations as a map
of differences from the default.
(require '[parkour.conf :as conf])
(conf/ig)
;;=> #hadoop.conf/configuration {}
(conf/assoc! *1 "foo" 1, "baz" [1 2 3])
;;=> #hadoop.conf/configuration {"baz" "1,2,3", "foo" "1"}
parkour.fs
The parkour.fs namespace contains functions for manipulating Hadoop Path and
FileSystem objects. It provides Hadoop integration for the standard library
IO facilities, functions simplifying common filesystem operations, and tagged
literal forms for Hadoop Path and java.net.URI objects.
(require '[parkour.fs :as fs])
(-> "*.clj" fs/path-glob first)
;;=> #hadoop.fs/path "file:/home/llasram/ws/parkour/project.clj"
(-> *1 slurp (subs 0 32))
;;=> "(defproject com.damballa/parkour"
parkour.tool
The parkour.tool namespace provides functions to wrap Clojure functions as
Hadoop Tools and to run Clojure functions via the Hadoop ToolRunner. This
provides a simple standard interface for implementing command-line tools which
allow end-users to set arbitrary Hadoop configuration parameters at run-time.
(require '[parkour.tool :as tool])
(defn tool
[conf & args]
...)
(defn -main
[& args]
(System/exit (tool/run tool args)))
parkour.wrapper
The parkour.wrapper namespace provides the Wrapper protocol, which allows
generic Clojure access to Writables and other mutable serialization classes
fitting the default Hadoop idiom.
The protocol unwrap function extracts a native value from a wrapper object.
The rewrap function mutates a wrapper object to wrap a new value. The
multimethod-backed new-instance function creates a fresh instance of an
arbitrary wrapper class, as per most Writables’ zero-argument constructors.
The parkour.wrapper namespaces contains implementations for many of the most
common classes, with others easily provided by the user. Much of the
namespace’s functionality has sensible default implementations, and the
auto-wrapper macro can automatically define a Wrapper implementation for
many common types.
(require '[parkour.wrapper :as w])
(import '[org.apache.hadoop.io VLongWritable])
(w/auto-wrapper VLongWritable)
(w/new-instance VLongWritable)
;;=> #<VLongWritable 0>
(w/rewrap *1 31337)
;;=> #<VLongWritable 31337>
(w/unwrap *1)
;;=> 31337
parkour.mapreduce
The parkour.mapreduce namespace provides Clojure MapReduce task integration,
allowing MapReduce tasks to be implemented by Clojure functions bounds to vars.
For each supported Hadoop base class, Parkour internally maintains pools of
static classes which use their Hadoop Configurations to specify a namespace
and var to load and invoke. The mapper!, reducer!, etc binding functions
modify a Configuration (or Job) to allocate a class of the associated type
and bind it to a particular var.
These functions do not directly configure a job to use the class. Instead,
they return the configured class, allowing the user to then pass that class to
any function or Hadoop method expecting a class of that type. These include the
standard .setMapperClass etc methods, but also such methods as
MultipleInputs’ .addInputPath. This approach allows Parkour to work cleanly
with Hadoop interfaces which use multiple classes of the same abstract type, as
with MultipleInputs’ use of multiple Mapper classes.
The invoked vars follow a uniform higher-order function interface, where during
task-setup Parkour invokes the var-functions with the job Configuration
followed by any (EDN-serialized) class-var binding arguments. The var-functions
then return functions (or other objects) which implement the functionality of
the bound class. The interfaces for these functions are class-specific, and
documented in the docstrings of the associated binding functions.
Because this interface can be unwieldy, Parkour transforms most var-bound
objects via “adapter functions.” Each var-type mapping has a type-specific
default adapter; alternative adapters may be provided by the user via the value
of the var’s :parkour.mapreduce/adapter metadata.
The namespace also provides functions for efficiently reshaping both task input and output collections, and for writing output tuples to the task context. Unlike the Java raw MapReduce interfaces, Parkour provides access to each individual input tuple in both map and reduce tasks, as collections of these tuples. The input reshaping functions allow access to key/value tuples, just keys or values, or – in reduce tasks – any combination of distinct grouping keys and reshaped sub-collections of grouped tuples.
Generally the reshaping functions do not need to be invoked directly, and
instead can be specified as keyword values to the ::mr/source-as and
::mr/sink-as metadata keys used by the collfn adapter.
(require '[clojure.core.reducers :as r])
(require '[parkour.mapreduce :as mr])
(defn replacer
{::mr/source-as :keys, ::mr/sink-as :keyvals}
[val coll]
(r/map (fn [key] [key val]) coll))
(defn run-replacer
[val]
(let [job (mr/job)]
(doto job
;; Set job name, JAR, input format, etc
...
;; Set mapper via Parkour
(.setMapperClass (mr/mapper! job #'replacer val))
(.setNumReduceTasks 0))
(.waitForCompletion job)))
Job configuration
Parkour provides a higher-level API for configuring Hadoop jobs in terms of “configuration steps.” Configuration steps are simply functions which invoke Hadoop- or library-provided methods for job configuration, but which capture step parameters as closed-over variables, exposing an entirely uniform interface for applying configuration steps to jobs. This allows code to treat configuration steps as first-class entities, inverting the control pattern exposed by the base Hadoop Java API.
parkour.cser
The parkour.cser namespace provides functions for writing/reading Clojure
values to/from job configuration parameters as extended EDN:
assoc!– Sets a configuration parameter to the EDN representation of a value.get– Reads a value from the EDN representation stored in a configuration parameter.
In addition to the obvious, these functions provide the following behaviors:
- For the dynamic scope of calls to
assoc!,parkour.cser/*conf*will be bound to the configuration being modified. This providesprint-methodimplementations access to the configuration a value is being serialized into, allowing them to access and modify other parameters as necessary to support their serialization. - The printed form of Clojure vars (e.g.
#'foo/bar) is read asrequireing the containing namespace thenresolveing the symbol. This supports e.g. higher-order tasks parameterized with other function-vars.
parkour.cstep
The parkour.cstep namespace provides the ConfigStep protocol. Although
configuration steps are conceptually just functions over Job objects,
implementation in terms of a protocol yields certain practical advantages. The
namespace provides the protocol-backed apply! function for applying an
arbitrary configuration step to a job.
The namespace also provides a few base implementations of the ConfigStep
protocol, which cover the majority of common uses:
- Functions – All single-argument functions are configuration steps, applied by
invoking the function on a provided
Jobinstance. - Maps – Clojure maps are applied by setting the value of each
Job-parameter map key to the associated map value. - Vectors – Clojure vectors are applied by applying each vector member as a configuration step, allowing easy composition of configuration step.
(require '[parkour.config :as config])
(require '[parkour.mapreduce :as mr])
(require '[parkour.cstep :as cstep])
(->> [{"foo" "bar"}
(fn [job] (conf/assoc! job "baz" "quux"))]
(cstep/apply! (mr/job)))
;;=> #hadoop.mapreduce/job {"baz" "quux", "foo" "bar"}
parkour.io.*
The collection of namespaces under parkour.io extends configuration steps with
specific integration for job input and output.
The parkour.io.dseq namespace provides the dseq function for reifying a
job-input configuration step as a “distributed sequence.” As with any other
configuration step, the backing configuration step functions simply call
underlying job-configuration methods.
In addition to acting as the configuration steps they wrap, distributed
sequences also act as locally-reducible collections. This provides seamless
local access to the input or output of any Hadoop job. Distributed sequences
may be directly reduced as unwrapped tuples, or opened with the
dseq/source-for function. The source-for function takes an optional :raw?
keyword-argument which allows direct access to the raw Hadoop wrapper type
instances, and its return value is seqable as well as reduce-able.
The parkour.io.dsink namespace provides the dsink function for reifying a
job-output configuration step as a “distributed sink.” As with dseqs, dsinks
simply wrap functions calling existing Hadoop methods. The dsink function
however takes a second parameter, which is a dseq for consuming as input the
results of a job writing to the dsink. This allows abstract specification of
job-chains, where subsequent jobs consume as input the output of previous jobs.
When called on a dsink, the dseq function will return this mirroring dseq.
The dsink/sink-for function allows any dsink to be opened for writing
locally. The same mr/sink function used in job task functions will write
local collections to these local sinks, producing the same output as when
running a job. This simplifies the creation of test fixtures, etc.
The parkour.io.dval namespace provides a value-oriented interface for
interacting with the Hadoop distributed cache. It is described in detail in the
documentation for distributed values.
Most of the remaining namespaces provide pre-built dseq and dsink implementations for common input and output formats:
parkour.io.text– Line-oriented text files.parkour.io.nline– Line-oriented text files, withnlines per map task.parkour.io.seqf– Hadoop sequence files.parkour.io.cascading– Cascading sequence files.parkour.io.mem– Memory-based input, for testing.parkour.io.avro– Clojure-customized Avro input and output via Abracad. In addition todseqanddsinkfunctions for creating Avro dseqs and dsinks, also provides ashufflefunction for configuring an Avro-serialized shuffle.
Two namespaces provide general facilities, distinct from particular concrete input or output formats:
parkour.io.mux– General multiple inputs; andparkour.io.dux– General multiple outputs.
The multiple inputs/outputs provided by these namespaces differ from the
standard Hadoop MultipleInputs and MultipleOutputs classes by proxying the
full InputFormat and OutputFormat interfaces across sub-format instances
configured with complete distinct sub-configurations. This allows different
inputs/outputs to use different values for any parameter isolated to the
input/output format. This isolation removes the need for special-purpose
de/muxing classes like AvroMultipleOutputs.
(require '[clojure.core.reducers :as r])
(require '[parkour.io.text :as text])
(->> (text/dseq "project.clj")
(r/map #(subs % 0 32))
(r/take 1)
(into []))
;;=> ["(defproject com.damballa/parkour"]
;; Or with multiple inputs
(->> (apply text/dseq (fs/path-glob "*.clj"))
(r/map #(subs % 0 32))
(r/take 1)
(into [])))
;;=> ["(defproject com.damballa/parkour"]
Job graph API
The parkour.graph namespace provides the Parkour job graph API. The job graph
API is a functional internal DSL for assembling configuration steps into job
nodes containing complete job configurations, and for assembling job nodes into
job graphs of multi-job processing pipelines.
A job node consists primarily of a list of configuration steps, a list of dependency job nodes, and a job stage. The job stage captures the state of a job node in the course of a relatively linear process of adding all the configuration steps necessary to produce a complete job. Each stage has an associated API function which produces a node in that stage while adding an associated configuration step.
The job graph API functions and associated stages are as follows:
input– Accepts an input dseq; returns a new:input-stage node with the dseq as its initial step. This is the sole graph API function which does not act on an existing job node.map– Accepts a:inputnode and a map-task var or class; returns a:mapnode with a configuration step specifying that mapper. Instead of a single:inputnode, also accepts a vector of:inputnodes, which are configured as multiplex input to the mapper.partition– Accepts a:mapnode, a shuffle configuration step, and an optional partitioner var or class; returns a:partitionnode. Instead of a single:mapnode, also accepts a vector of:mapnodes, which are configured as multiplex input to the partitioner. Instead of a configuration step, also accepts a vector of two classes, which are configured as the map output key and value classes for a basic shuffle.combine– Accepts a:partitionnode and a combine-task var or class; returns a:combinenode. The provided combiner will double as the job reducer if a separate reducer is not later specified.reduce– Accepts a:partitionor:combinenode and a reduce-task var or class; returns a:reducenode.output– Accepts a:mapor:combine/:reducenode and an output dsink; returns a:inputnode which consumes from the provided dsink’s associated dseq and depends on the job node completed by the dsink’s configuration step. Instead of a single output dsink, also accepts multiple arguments as a sequence of name-dsink outputs, which are configured as demultiplex named outputs; returns a vector of:inputnodes for the associated dseqs.
In addition to the per-stage functions, the job graph API also provide a generic
config function, which adds arbitrary configuration steps to a node in any
stage.
The job graph API execute function will execute the jobs produced by a graph
of job nodes. The execute function accepts either a single job graph leaf
node or a vector of such nodes, a base Configuration, and a job basename. It
runs the jobs composing the graph, attempting to run independent jobs in
parallel. On successful completion, it returns a vector of the provided leaf
node dseqs, and on failure throws an exception.
For the common case where a job graph yield only a single output, the fexecute
function will verify that there is only a single output and return it.
(defn word-count
[conf lines]
(-> (pg/input lines)
(pg/map #'word-count-m)
(pg/partition [Text LongWritable])
(pg/combine #'ptb/keyvalgroups-r #'+)
(pg/output (seqf/dsink [Text LongWritable]))
(pg/fexecute conf `word-count)))