Showing posts with label content-management. Show all posts
Showing posts with label content-management. Show all posts

Wednesday, January 01, 2014

Akka Content Ingestion Pipeline, Part IV


Happy New Year! Over the last three posts, I've described an Akka based Content Ingestion Pipeline modelled after the NutchGORA pipeline. This pipeline was my vehicle for learning Akka, something I've been planning to do for a while. In this post (the final installment on this series, at least so far), I explore how to distribute this application horizontally across multiple machines (scale out).

Akka is distributed by design. The code I've built so far can be regarded as a single server version of a distributed system. According to the Akka remoting documentation (emphasis mine):

Everything in Akka is designed to work in a distributed setting: all interactions of actors use purely message passing and everything is asynchronous. This effort has been undertaken to ensure that all functions are available equally when running within a single JVM or on a cluster of hundreds of machines. The key for enabling this is to go from remote to local by way of optimization instead of trying to go from local to remote by way of generalization.

Akka can work with remote Actors in two ways, either by looking them up in a remote ActorSystem, or by creating them in the remote ActorSystem. I use the latter approach. The components that do the heavy lifting in the pipeline are the workers, and scaling out to handle more incoming requests would imply increasing the number of workers or making them faster, both of which can be done by giving them their own dedicated hardware.

The architecture diagram has been updated with the distribution boundaries, they are indicated by the gray boxes below. The master node is the large gray box on the top, and contains the REST Interface, the Controller and the Router actors. The worker nodes (can be an array of nodes for each worker class) are the ones that wrap the Fetch, Parse and Index worker arrays.


Each of these nodes are wrapped in an Akka ActorSystem, which can be accessed by an URI from other ActorSystems. So in addition to the HTTP interface that the master node exposes to the outside world, it also exposes a host:port and has a name that other Akka ActorSystems can use to communicate with it.

For testing, I configured the pipeline with just 2 ActorSystems - the master node listening on localhost:2552 and identified by URI akka.tcp://DelSym@localhost:2552, and one remote node listening on localhost:2553 and identified by URI akka.tcp://remote@localhost:2553. Here is some code to create a named (name supplied from command line) remote Akka ActorSystem using configuration parameters in the remote.conf file.

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Source: src/main/scala/com/mycompany/delsym/remote/RemoteAkka.scala
package com.mycompany.delsym.remote

import akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory

object RemoteAkka extends App {

  val name = if (args.isEmpty) "remote" else args(0)
  
  val conf = ConfigFactory.load("remote")
  val host = conf.getString("akka.remote.netty.tcp.hostname")
  val port = conf.getInt("akka.remote.netty.tcp.port")
  
  val system = ActorSystem(name, conf)
  Console.println("Remote system [%s] listening on %s:%d"
    .format(name, host, port))
  
  sys.addShutdownHook {
    Console.println("Shutting down Remote Akka")
    system.shutdown
  }
}

The remote.conf file looks like this. This is meant to be used in order to start up ActorSystems on multiple nodes in a network.

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Source: src/main/resources/remote.conf
akka {
  log-dead-letters-during-shutdown = off
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
  }
  remote {
    enabled-transports = ["akka.remote.netty.tcp"]
    netty.tcp {
      hostname = "127.0.0.1"
      port = 2553
    }
  }
}

I then added a property in application.conf to specify a list of ActorSystem URIs for the routers. The routers are Round Robin routers, so giving them a list of ActorSystem URIs will cause them to cycle through the URIs, creating remote Actors and distributing evenly across multiple remote ActorSystems. The Controller Actor code (which instantiates the routers) has been modified to create local workers if the node list is empty and remote workers otherwise. The updated code for the Controller is shown below:

1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Source: src/main/scala/com/mycompany/delsym/actors/Controller.scala
package com.mycompany.delsym.actors

import scala.collection.JavaConversions.asScalaBuffer
import scala.concurrent.duration.DurationInt

import com.mycompany.delsym.daos.HtmlOutlinkFinder
import com.mycompany.delsym.daos.MockOutlinkFinder
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigList

import akka.actor.Actor
import akka.actor.ActorLogging
import akka.actor.AddressFromURIString
import akka.actor.OneForOneStrategy
import akka.actor.Props
import akka.actor.SupervisorStrategy
import akka.actor.actorRef2Scala
import akka.remote.routing.RemoteRouterConfig
import akka.routing.RoundRobinRouter
import akka.routing.RouterConfig

class Controller extends Actor with ActorLogging {

  override val supervisorStrategy = OneForOneStrategy(
      maxNrOfRetries = 10,
      withinTimeRange = 1.minute) {
    case _: Exception => SupervisorStrategy.Restart
  }
  
  val reaper = context.actorOf(Props[Reaper], name="reaper")

  val conf = ConfigFactory.load()
  val numFetchers = conf.getInt("delsym.fetchers.numworkers")
  val fetchNodes = conf.getList("delsym.fetchers.nodes")
  
  val numParsers = conf.getInt("delsym.parsers.numworkers")
  val parseNodes = conf.getList("delsym.parsers.nodes")
  
  val numIndexers = conf.getInt("delsym.indexers.numworkers")
  val indexNodes = conf.getList("delsym.indexers.nodes")
  
  val testUser = conf.getBoolean("delsym.testuser")
  val outlinkFinder = if (testUser) new MockOutlinkFinder()
                      else new HtmlOutlinkFinder()
  
  val queueSizes = scala.collection.mutable.Map[String,Long]()
  
  val fetchers = context.actorOf(Props[FetchWorker]
    .withRouter(buildRouter(numFetchers, fetchNodes)), 
    name="fetchers")
  reaper ! Register(fetchers)
  queueSizes += (("fetchers", 0L))

  val parsers = context.actorOf(Props[ParseWorker]
    .withRouter(buildRouter(numParsers, parseNodes)), 
    name="parsers")
  reaper ! Register(parsers)
  queueSizes += (("parsers", 0L))
  
  val indexers = context.actorOf(Props[IndexWorker]
    .withRouter(buildRouter(numIndexers, indexNodes)),
    name="indexers")
  reaper ! Register(indexers)
  queueSizes += (("indexers", 0L))

  def receive = {
    case m: Fetch => {
      increment("fetchers")
      fetchers ! m
    }
    case m: FetchComplete => {
      decrement("fetchers")
      if (m.fwd) parsers ! Parse(m.url)
    }
    case m: Parse => {
      increment("parsers")
      parsers ! m
    }
    case m: ParseComplete => {
      decrement("parsers")
      outlinks(m.url).map(outlink => 
        fetchers ! Fetch(outlink._1, outlink._2, outlink._3))
      if (m.fwd) indexers ! Index(m.url)
    }
    case m: Index => {
      increment("indexers")
      indexers ! m
    }
    case m: IndexComplete => {
      decrement("indexers")
    }
    case m: Stats => {
      sender ! queueSize()
    }
    case m: Stop => {
      reaper ! Stop(0)
    }
    case _ => log.info("Unknown message received.")
  }
  
  def buildRouter(n: Int, nodes: ConfigList): RouterConfig = {
    if (nodes.isEmpty) RoundRobinRouter(n)
    else {
      val addrs = nodes.unwrapped()
        .map(node => node.asInstanceOf[String])
        .map(node => AddressFromURIString(node))
        .toSeq
      RemoteRouterConfig(RoundRobinRouter(n), addrs)
    }
  }
  
  def queueSize(): Stats = Stats(queueSizes.toMap)
  
  def outlinks(url: String): 
      List[(String,Int,Map[String,String])] = {
    outlinkFinder.findOutlinks(url) match {
      case Right(triples) => triples
      case Left(f) => List.empty
    }
  }
  
  def increment(key: String): Unit = {
    queueSizes += ((key, queueSizes(key) + 1))
  }
  
  def decrement(key: String): Unit = {
    queueSizes += ((key, queueSizes(key) - 1))
  }
}

The documentation indicates that a better approach would be to declare the routers in configuration, so the local configuration would be different from the distributed configuration. I did not do this because my test case refers to the routers as /controller/* but the actual code refers to it as /api/controller/* (I should probably change the test code but I was too lazy). But in any case, changing from a local to a remote router configuration is simply a matter of wrapping the Router Configuration with a RemoteRouterConfig (buildRouter function in the code above), so this approach works fine also.

Going from local to remote also requires you to think about serialization. I have chosen to use Java serialization, and I have configured Akka (via the application.conf file) to automatically use Java serialization for my messages. In addition, the distributed version of the master ActorSystem also exposes its own address in the configuration and sets the provider to a remote ActorRef provider. The other important difference is the non empty nodes list value under the delsym namespace for each of the fetcher, parser and indexer. The remote configuration is shown below:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Source: src/main/resources/application.conf.remote
akka {
  loglevel = INFO
  stdout-loglevel = INFO
  akka.loggers = ["akka.event.slf4j.Slf4jLogger"]
  log-dead-letters-during-shutdown = off
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
    remote {
      enabled-transports = ["akka.remote.netty.tcp"]
      netty.tcp {
        hostname = "127.0.0.1"
        port = 2552
      }
    }
    serializers {
      java = "akka.serialization.JavaSerializer"
    }
    serialization-bindings {
      "com.mycompany.delsym.actors.DelsymMessage" = java
    }
  }
}

spray {
  can {
    server {
      server-header = "DelSym REST API"
    }
  }
}

delsym {
  testuser = true
  fetchers {
    numworkers = 5
    refreshIntervalDays = 30
    numRetries = 3
    nodes = ["akka.tcp://remote@127.0.0.1:2553"]
  }
  parsers {
    numworkers = 5
    nodes = ["akka.tcp://remote@127.0.0.1:2553"]
  }
  indexers {
    numworkers = 5
    nodes = ["akka.tcp://remote@127.0.0.1:2553"]
  }
  mongodb {
    host = "127.0.0.1"
    port = 27017
    dbname = "delsymdb"
    collname = "documents"
  }
  solr {
    server = "http://127.0.0.1:8983/solr/collection1/"
    dbfieldnames = "_id,url,p_title,p_author,textContent"
    solrfieldnames = "id,url,title,author,text"
    commitInterval = 10
  }
  rest {
    host = "127.0.0.1"
    port = 8080
    timeout = 1
  }
}

There are now 3 versions of application.conf in the Delsym repo on GitHub. You will have to link to the correct one depending on whether you want to run the mock tests, run in local (all actors on single ActorSystem) or remote (multiple ActorSystems) mode.

The effort to build the code for this part of the pipeline was mostly conceptual, ie, understanding how the different components fit together. I found the following Akka reference pages very useful. The pages are all for Akka version 2.2.3 (latest stable version) that I used for this work - the default pages that show up (in response to a Google search for example) are for version 2.3 which is still in development. The 2.3 code is different enough for this detail to be annoying, so mentioning it here.


In addition, I also found the akka-sample-remote-scala useful, although the pattern shown there is slightly different from what I used. Another useful source was the Remoting chapter from the Akka Essentials book.

I was able to run the ActorFlowTest unit test with Mock workers (minus the asserts, since the workers update counters on the remote ActorSystem which I no longer have control over) and verify from the logs that the fetching, parsing and indexing happen on my remote ActorSystem at localhost:2553. The code also exits cleanly which means Deathwatch works fine with remote workers. However, I saw lots of messages sent to the dead-letter mailbox which I haven't been able to figure out yet (they are not application messages) - I will post an update here (and bugfix to the DelSym GitHub repo once I do.

Monday, September 03, 2007

More Jackrabbit - using XPath queries

Last week, I posted my initial experiences with Apache Jackrabbit. I have learned a little more since then, although its still not enough for me to consider moving all our content over to it. In my last post, I built up a content repository with the following design:

1
2
3
4
5
6
7
 rootnode
    |
    +-- ${contentType}
            |
            +-- ${contentId} {
                  properties {title:"foo", etc}
                }

This design works for getting back content by contentId only, since we can simply navigate down to the contentId node directly using code such as this:

1
2
3
4
5
6
7
8
    Node contentIdNode = session.getRootNode().getNode(contentSource).getNode(contentId);
    DataHolder dataHolder = new DataHolder();
    PropertyIterator pi = contentNode.getProperties();
    while (pi.hasNext()) {
      Property prop = pi.nextProperty();
      dataHolder.setProperty(prop.getName(), prop.getValue().getString());
    }
    return dataHolder;

However, this approach breaks down completely when we want to search by some other attribute, such as URL, which is a fairly common occurence for content based applications. But not to worry ... all JCR compliant repositories, including Jackrabbit, provides an interface to query nodes using XPath, which is described in some detail in this article. In addition, Jackrabbit also provides an SQL interface, which is described here.

To allow for this kind of searching, I had to change the way I had set up my content in the repository, something like this:

1
2
3
4
5
6
7
 rootnode
    |
    +-- ${contentType}
            |
            +-- content {
                  properties {contentId:1234, title:"foo", etc}
                }

In addition, I had to set up two SearchIndex XML blocks in the repository.xml, as a nested tag within Workspace and Repository. This is because the QueryManager uses the built in Lucene index internally to pull out information from the repository. I did not include this configuration initially because I thought that this was for searching through content (such as on a content search page), rather than doing lookups within content. The delta for the repository.xml to include the SearchIndex is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Repository>
  ...
  <Workspace name="${wsp.name}">
    ...
    <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
      <param name="path" value="${wsp.home}/index"/>
      <param name="useCompoundFile" value="true"/>
      <param name="minMergeDocs" value="100"/>
      <param name="volatileIdleTime" value="3"/>
      <param name="maxMergeDocs" value="100000"/>
      <param name="mergeFactor" value="10"/>
      <param name="maxFieldLength" value="10000"/>
      <param name="bufferSize" value="10"/>
      <param name="cacheSize" value="1000"/>
      <param name="forceConsistencyCheck" value="false"/>
      <param name="autoRepair" value="true"/>
      <param name="analyzer" value="org.apache.lucene.analysis.standard.StandardAnalyzer"/>
      <param name="queryClass" value="org.apache.jackrabbit.core.query.QueryImpl"/>
      <param name="respectDocumentOrder" value="true"/>
      <param name="resultFetchSize" value="2147483647"/>
      <param name="extractorPoolSize" value="0"/>
      <param name="extractorTimeout" value="100"/>
      <param name="extractorBackLogSize" value="100"/>
    </SearchIndex>
  </Workspace>

  ...
  <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
    <param name="path" value="${rep.home}/repository/index"/>
  </SearchIndex>
  
</Repository>

With the new repository design, the loader changed slightly to hardcode the node name for the content node, and to include the contentId as a property of the content node. I added a getContentByUrl(String contentSource, String url) method in addition to the original getContent(String contentSource, String contentId). In fact the getContent() method also changed to use the XPath approach. They are shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
  public DataHolder getContent(String contentSource, String contentId) throws Exception {
    if (session == null) {
      session = repository.login(new SimpleCredentials("user", "pass".toCharArray()));
    }
    DataHolder dataHolder = new DataHolder();
    Workspace workspace = session.getWorkspace();
    QueryManager queryManager = workspace.getQueryManager();
    Query query = queryManager.createQuery("//" + contentSource + 
      "/content[@contentId='" + contentId + "']", Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator ni = result.getNodes();
    while (ni.hasNext()) {
      Node contentNode = ni.nextNode();
      PropertyIterator pi = contentNode.getProperties();
      dataHolder.setProperty("contentId", contentId);
      dataHolder.setProperty("contentSource", contentSource);
      while (pi.hasNext()) {
        Property prop = pi.nextProperty();
        dataHolder.setProperty(prop.getName(), prop.getValue().getString());
      }
      break;
    }
    return dataHolder;
  }
  
  public List<DataHolder> getContentByUrl(String contentSource, String url) 
      throws Exception {
    List<DataHolder> contents = new ArrayList<DataHolder>();
    if (session == null) {
      session = repository.login(new SimpleCredentials("user", "pass".toCharArray()));
    }
    Workspace workspace = session.getWorkspace();
    QueryManager queryManager = workspace.getQueryManager();
    Query query = queryManager.createQuery("//" + contentSource + 
      "/content[@url='" + url + "']", Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator ni = result.getNodes();
    while (ni.hasNext()) {
      Node childNode = ni.nextNode();
      contents.add(getContent(contentSource, childNode.getName()));
    }
    return contents;
  }

I haven't actually tried the SQL interface, but from the link to the mail, it should not be too hard to use that either. However, the SQL interface is Jackrabbit specific, so its probably not such a good idea if one wants to migrate to a different JCR compliant repository in the future.

I do plan on doing some more reading and experimenting with Jackrabbit, from the list of articles on this Jackrabbit Wiki page. If I find anything interesting, I will write about it.

Saturday, January 13, 2007

Taxonomy, Ontology and Facets

I have a confession to make. For the last 5 years or so, I have been working with Taxonomies and Ontologies without fully understanding what they are. Fortunately, as a web developer (my last job), they were peripheral to what I did, and there were APIs which hid the underlying structure, so not understanding them was not as clueless as it now seems in retrospect. At my current job, however, I am working with the company's information assets more closely than I was before, so this understanding is a requirement. This article describes the current state of my understanding. I am by no means an expert on these concepts, but I think the information here may be useful to a web developer who is curious about Taxonomies, Ontologies and Facets, and how they can be used to surface information on web pages.

Before I started reading up, I would describe a Taxonomy as a classification of entities according to some preferred classification property, and an Ontology as a re-classification of the entities in the Taxonomy according to some other property (or properties) that we wanted to show the user of our web pages. So for example, the Taxonomy for an electronics store may be the various suppliers at the top, followed by the different types of items each company manufactures, followed by the items themselves. This classification may have been natural because the Taxonomy reflected the needs of how they got the specifications for their products (by supplier), but down the road, they would probably want to show the customer all the HDTVs they sell together, regardless of who manufactured it. So they would create another classification of their products by category. There may be many such classifications, so their Ontology may end up as a forest of trees.

As surprising as it seems, the above is largely correct. However, notice the relative lack of specifics. Notice also how the explanation above requires a large amount of hand-waving to explain it and does not delve too deeply about how it can be implemented, a sure sign that the person explaining it needs to refine his understanding of the subject.

I found the clearest and most concise (to me anyway) explanation about how a Taxonomy and Ontology differ from each other here. As this explanation states, a Taxonomy is generally modelled as a Tree that may or may not support multiple inheritance, so the only relationships between entities that can be inferred are Broader (parent or parents of the current entity), Narrower (children of the current entity) and Related (siblings of the current entity). Ontologies, on the other hand are modelled as triples (source entity, relation, target entity). So the "Dogs LivesIn House" relationship would be the triple (Dog, lives in, House).

A Facet is a property that can be applied to all or some of the entities in the taxonomy. It is a one-dimensional cross cut, and does not specify a relation by itself. A good introduction to Facets for a web developer is William Denton's article - "How to make a Faceted Classification and put it on the Web". The closest parallel to Facets I could think of was the use of star schemas in Data Warehousing.

Ontologies can be modelled in various ways. If the Ontology is used to recategorize the entities, then it may make sense to build a set of parallel tree structures based on the Taxonomy. With this approach, however, the Ontology becomes as difficult to change and enhance as the Taxonomy. Fortunately, this is not necessary most of the time. Most Ontology trees that back web applications are shallow and broad, because you want to provide the maximum data with the simplest possible interface to the client without being irrelevant. So mostly, all we need to do is to enrich the Taxonomy by providing additional relations, and Facets are usually sufficient and very well-suited for this purpose.

In a computer system, you would typically model the tree structure for a Taxonomy using a relational database. To model an Ontology relation, you could model each relation as a separate table with a many-to-many relation foreign key references back to the entity table from the taxonomy.

1
2
entity (id, entity_details...)
livesIn (lhs_entity_id, rhs_entity_id)

If there are many different relationships, it may make more sense to combine them into a single table and use a relation_id in a relations table instead.

1
2
3
entity (id, entity_details...)
relationships (id, relationship_details...)
entity_relations (lhs_entity_id, relationship_id, rhs_entity_id)

So to find what are the possible places the entity "Dog" can live in, we would find the entity_id for "Dog", the relationship_id for "livesIn", and find the entities that are related to Dog in this manner from the entity_relations table.

1
2
3
4
5
6
7
select er.*
  from entity el, entity er, relationships r, entity_relations re
  where el.id = re.lhs_entity_id
  and r.id = re.relationship_id
  and er.id = re.rhs_entity_id
  and el.name = "Dog"
  and r.name = "LivesIn" 

In the above example, we modelled our triple with the entity_relations table. A Facet is technically a property, which is what the relation part of our triple is. However, it is associated with an entity, which gives us the left hand side entity of our triple, and has a value. The value could be anything, but in cases where we control the creation of the Facet and its values, it could very well point to an entity in our dataset, which would be the same as the right hand side entity relation of our triple.