Hadoop Common
  1. Hadoop Common
  2. HADOOP-811

Patch to support multi-threaded MapRunnable

    Details

    • Type: New Feature New Feature
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 0.10.0
    • Fix Version/s: 0.10.0
    • Component/s: None
    • Labels:
      None
    • Environment:

      all

      Description

      The MapRunner calls Mapper.map in a serialized fashion.

      This is suitable for CPU/memory bound operations.

      However, when doing IO bound operations this serialization affects the throughput significantly.

      In order to support IO bound operations more efficiently I've implemented a multithreaded MapRunnable.

      Following is the implementation of this MapRunnable, MultithreadedMapRunner.

      I've only had to modify on method in the existing code (in the Task class) to avoid data corruption in the reporter.

      Index: Task.java
      ===================================================================
      — Task.java (revision 485492)
      +++ Task.java (working copy)
      @@ -153,9 +153,11 @@
      public Reporter getReporter(final TaskUmbilicalProtocol umbilical,
      final Progress progress) throws IOException {
      return new Reporter() {

      • public void setStatus(String status) throws IOException {
      • progress.setStatus(status);
      • progress();
        + public void setStatus(String status) throws IOException
        Unknown macro: {+ synchronized (this) { + progress.setStatus(status); + progress(); + } }

        public void progress() throws IOException {
        reportProgress(umbilical);

      -----------------------------------------------------------
      MultithreadedMapRunner.java

      package org.apache.hadoop.mapred;

      import org.apache.hadoop.util.ReflectionUtils;
      import org.apache.hadoop.io.WritableComparable;
      import org.apache.hadoop.io.Writable;
      import org.apache.commons.logging.Log;
      import org.apache.commons.logging.LogFactory;

      import java.io.IOException;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      import java.util.concurrent.TimeUnit;

      /**

      • Multithreaded implementation for @link org.apache.hadoop.mapred.MapRunnable.
      • <p>
      • It can be used instead of the default implementation,
      • @link org.apache.hadoop.mapred.MapRunner, when the Map operation is not CPU
      • bound in order to improve throughput.
      • <p>
      • Map implementations using this MapRunnable must be thread-safe.
      • <p>
      • The Map-Reduce job has to be configured to use this MapRunnable class (using
      • the <b>mapred.map.runner.class</b> property) and
      • the number of thread the thread-pool can use (using the
      • <b>mapred.map.multithreadedrunner.threads</b> property).
      • <p>
        *
      • @author Alejandro Abdelnur
        */
        public class MultithreadedMapRunner implements MapRunnable {
        private static final Log LOG =
        LogFactory.getLog(MultithreadedMapRunner.class.getName());

      private JobConf job;
      private Mapper mapper;
      private ExecutorService executorService;
      private IOException ioException;

      public void configure(JobConf job) {
      int numberOfThreads =
      job.getInt("mapred.map.multithreadedrunner.threads", 10);
      if (LOG.isDebugEnabled())

      { LOG.debug("Configuring job " + job.getJobName() + " to use " + numberOfThreads + " threads" ); }

      this.job = job;
      this.mapper = (Mapper)ReflectionUtils.newInstance(job.getMapperClass(),
      job);

      // Creating a threadpool of the configured size to execute the Mapper
      // map method in parallel.
      executorService = Executors.newFixedThreadPool(numberOfThreads);
      }

      public void run(RecordReader input, OutputCollector output,
      Reporter reporter)
      throws IOException {
      try {
      // allocate key & value instances these objects will not be reused
      // because execution of Mapper.map is not serialized.
      WritableComparable key = input.createKey();
      Writable value = input.createValue();

      while (input.next(key, value)) {

      // Run Mapper.map execution asynchronously in a separate thread.
      // If threads are not available from the thread-pool this method
      // will block until there is a thread available.
      executorService.execute(
      new MTMapperRunable(key, value, output, reporter));

      // Checking if a Mapper.map within a Runnable has generated an
      // IOException. If so we rethrow it to force an abort of the Map
      // operation thus keeping the semantics of the default
      // implementation.
      if (ioException != null)

      { throw ioException; }

      // Allocate new key & value instances as mapper is running in parallel
      key = input.createKey();
      value = input.createValue();
      }

      if (LOG.isDebugEnabled()) { LOG.debug("Finished dispatching all Mappper.map calls, job " + job.getJobName()); }

      // Graceful shutdown of the Threadpool, it will let all scheduled
      // Runnables to end.
      executorService.shutdown();

      try {

      // Now waiting for all Runnables to end.
      while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
      if (LOG.isDebugEnabled()) { LOG.debug("Awaiting all running Mappper.map calls to finish, job " + job.getJobName()); }

      // Checking if a Mapper.map within a Runnable has generated an
      // IOException. If so we rethrow it to force an abort of the Map
      // operation thus keeping the semantics of the default
      // implementation.
      // NOTE: while Mapper.map dispatching has concluded there are still
      // map calls in progress.
      if (ioException != null) { throw ioException; }
      }

      // Checking if a Mapper.map within a Runnable has generated an
      // IOException. If so we rethrow it to force an abort of the Map
      // operation thus keeping the semantics of the default
      // implementation.
      // NOTE: it could be that a map call has had an exception after the
      // call for awaitTermination() returing true. And edge case but it
      // could happen.
      if (ioException != null) { throw ioException; }

      }
      catch (IOException ioEx)

      { // Forcing a shutdown of all thread of the threadpool and rethrowing // the IOException executorService.shutdownNow(); throw ioEx; }

      catch (InterruptedException iEx)

      { throw new IOException(iEx.getMessage()); }

      } finally

      { mapper.close(); }

      }

      /**

      • Runnable to execute a single Mapper.map call from a forked thread.
        */
        private class MTMapperRunable implements Runnable {
        private WritableComparable key;
        private Writable value;
        private OutputCollector output;
        private Reporter reporter;

      /**

      • Collecting all required parameters to execute a Mapper.map call.
      • <p>
        *
      • @param key
      • @param value
      • @param output
      • @param reporter
        */
        public MTMapperRunable(WritableComparable key, Writable value,
        OutputCollector output, Reporter reporter) { this.key = key; this.value = value; this.output = output; this.reporter = reporter; }

      /**

      • Executes a Mapper.map call with the given Mapper and parameters.
      • <p>
      • This method is called from the thread-pool thread.
        *
        */
        public void run() {
        try { // map pair to output MultithreadedMapRunner.this.mapper.map(key, value, output, reporter); }

        catch (IOException ex) {
        // If there is an IOException during the call it is set in an instance
        // variable of the MultithreadedMapRunner from where it will be
        // rethrown.
        synchronized (MultithreadedMapRunner.this)

        Unknown macro: { if (MultithreadedMapRunner.this.ioException == null) { MultithreadedMapRunner.this.ioException = ex; } }

        }
        }
        }

      }

      1. MultithreadedMapRunner.java
        6 kB
        Alejandro Abdelnur
      2. MultithreadedMapRunner.java
        7 kB
        Alejandro Abdelnur
      3. diff.txt
        7 kB
        Alejandro Abdelnur
      4. diff.txt
        8 kB
        Alejandro Abdelnur

        Activity

        Hide
        Doug Cutting added a comment -

        I just committed this. Thanks, Alejandro!

        Show
        Doug Cutting added a comment - I just committed this. Thanks, Alejandro!
        Hide
        Owen O'Malley added a comment -

        +1
        This looks good. Thanks.

        Show
        Owen O'Malley added a comment - +1 This looks good. Thanks.
        Hide
        Hadoop QA added a comment -

        +1, since http://issues.apache.org/jira/secure/attachment/12347069/diff.txt applied and successfully tested against trunk revision r486810.

        Show
        Hadoop QA added a comment - +1, since http://issues.apache.org/jira/secure/attachment/12347069/diff.txt applied and successfully tested against trunk revision r486810.
        Hide
        Alejandro Abdelnur added a comment -

        and now, indented.

        Show
        Alejandro Abdelnur added a comment - and now, indented.
        Hide
        Alejandro Abdelnur added a comment -

        Attached is the 'svn diff'

        Show
        Alejandro Abdelnur added a comment - Attached is the 'svn diff'
        Hide
        Devaraj Das added a comment -

        You copy-pasted the patch as a comment on this issue. Could you please create a file out of this copy-paste stuff and upload that?

        Show
        Devaraj Das added a comment - You copy-pasted the patch as a comment on this issue. Could you please create a file out of this copy-paste stuff and upload that?
        Hide
        Alejandro Abdelnur added a comment -

        Modified following comment recommendations.

        Show
        Alejandro Abdelnur added a comment - Modified following comment recommendations.
        Hide
        Alejandro Abdelnur added a comment -

        Following is the full 'svn diff' as requested.

        I'm also re-attaching the changed (per comments) MultithreadedMapRunner.

        Index: src/java/org/apache/hadoop/mapred/Task.java
        ===================================================================
        — src/java/org/apache/hadoop/mapred/Task.java (revision 486454)
        +++ src/java/org/apache/hadoop/mapred/Task.java (working copy)
        @@ -154,8 +154,10 @@
        final Progress progress) throws IOException {
        return new Reporter() {
        public void setStatus(String status) throws IOException {

        • progress.setStatus(status);
        • progress();
          + synchronized (this) { + progress.setStatus(status); + progress(); + }

          }
          public void progress() throws IOException {
          reportProgress(umbilical);
          Index: src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java
          ===================================================================

            • src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java (revision 0)
              +++ src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java (revision 0)
              @@ -0,0 +1,199 @@
              +package org.apache.hadoop.mapred.lib;
              +
              +import org.apache.hadoop.util.ReflectionUtils;
              +import org.apache.hadoop.io.WritableComparable;
              +import org.apache.hadoop.io.Writable;
              +import org.apache.hadoop.mapred.MapRunnable;
              +import org.apache.hadoop.mapred.JobConf;
              +import org.apache.hadoop.mapred.Mapper;
              +import org.apache.hadoop.mapred.RecordReader;
              +import org.apache.hadoop.mapred.OutputCollector;
              +import org.apache.hadoop.mapred.Reporter;
              +import org.apache.commons.logging.Log;
              +import org.apache.commons.logging.LogFactory;
              +
              +import java.io.IOException;
              +import java.util.concurrent.ExecutorService;
              +import java.util.concurrent.Executors;
              +import java.util.concurrent.TimeUnit;
              +
              +/**
              + * Multithreaded implementation for @link org.apache.hadoop.mapred.MapRunnable.
              + * <p>
              + * It can be used instead of the default implementation,
              + * @link org.apache.hadoop.mapred.MapRunner, when the Map operation is not CPU
              + * bound in order to improve throughput.
              + * <p>
              + * Map implementations using this MapRunnable must be thread-safe.
              + * <p>
              + * The Map-Reduce job has to be configured to use this MapRunnable class (using
              + * the <b>mapred.map.runner.class</b> property) and
              + * the number of thread the thread-pool can use (using the
              + * <b>mapred.map.multithreadedrunner.threads</b> property).
              + * <p>
              + *
              + * @author Alejandro Abdelnur
              + */
              +public class MultithreadedMapRunner implements MapRunnable {
              + private static final Log LOG =
              + LogFactory.getLog(MultithreadedMapRunner.class.getName());
              +
              + private JobConf job;
              + private Mapper mapper;
              + private ExecutorService executorService;
              + private volatile IOException ioException;
              +
              + public void configure(JobConf job)
              Unknown macro: {+ int numberOfThreads =+ job.getInt("mapred.map.multithreadedrunner.threads", 10);+ if (LOG.isDebugEnabled()) { + LOG.debug("Configuring job " + job.getJobName() + + " to use " + numberOfThreads + " threads" ); + }++ this.job = job;+ this.mapper = (Mapper)ReflectionUtils.newInstance(job.getMapperClass(),+ job);++ // Creating a threadpool of the configured size to execute the Mapper+ // map method in parallel.+ executorService = Executors.newFixedThreadPool(numberOfThreads);+ }

              +
              + public void run(RecordReader input, OutputCollector output,
              + Reporter reporter)
              + throws IOException {
              + try {
              + // allocate key & value instances these objects will not be reused
              + // because execution of Mapper.map is not serialized.
              + WritableComparable key = input.createKey();
              + Writable value = input.createValue();
              +
              + while (input.next(key, value))

              Unknown macro: {++ // Run Mapper.map execution asynchronously in a separate thread.+ // If threads are not available from the thread-pool this method+ // will block until there is a thread available.+ executorService.execute(+ new MapperInvokeRunable(key, value, output, reporter));++ // Checking if a Mapper.map within a Runnable has generated an+ // IOException. If so we rethrow it to force an abort of the Map+ // operation thus keeping the semantics of the default+ // implementation.+ if (ioException != null) { + throw ioException; + }
              +
              + // Allocate new key & value instances as mapper is running in parallel
              + key = input.createKey();
              + value = input.createValue();
              + }
              +
              + if (LOG.isDebugEnabled()) { + LOG.debug("Finished dispatching all Mappper.map calls, job " + + job.getJobName()); + }
              +
              + // Graceful shutdown of the Threadpool, it will let all scheduled
              + // Runnables to end.
              + executorService.shutdown();
              +
              + try {
              +
              + // Now waiting for all Runnables to end.
              + while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
              + if (LOG.isDebugEnabled()) { + LOG.debug("Awaiting all running Mappper.map calls to finish, job " + + job.getJobName()); + }
              +
              + // Checking if a Mapper.map within a Runnable has generated an
              + // IOException. If so we rethrow it to force an abort of the Map
              + // operation thus keeping the semantics of the default
              + // implementation.
              + // NOTE: while Mapper.map dispatching has concluded there are still
              + // map calls in progress.
              + if (ioException != null) { + throw ioException; + }
              + }
              +
              + // Checking if a Mapper.map within a Runnable has generated an
              + // IOException. If so we rethrow it to force an abort of the Map
              + // operation thus keeping the semantics of the default
              + // implementation.
              + // NOTE: it could be that a map call has had an exception after the
              + // call for awaitTermination() returing true. And edge case but it
              + // could happen.
              + if (ioException != null) { + throw ioException; + }+ }

              + catch (IOException ioEx)

              { + // Forcing a shutdown of all thread of the threadpool and rethrowing + // the IOException + executorService.shutdownNow(); + throw ioEx; + }

              + catch (InterruptedException iEx)

              { + throw new IOException(iEx.getMessage()); + }

              +
              + } finally

              { + mapper.close(); + }

              + }
              +
              +
              + /**
              + * Runnable to execute a single Mapper.map call from a forked thread.
              + */
              + private class MapperInvokeRunable implements Runnable {
              + private WritableComparable key;
              + private Writable value;
              + private OutputCollector output;
              + private Reporter reporter;
              +
              + /**
              + * Collecting all required parameters to execute a Mapper.map call.
              + * <p>
              + *
              + * @param key
              + * @param value
              + * @param output
              + * @param reporter
              + */
              + public MapperInvokeRunable(WritableComparable key, Writable value,
              + OutputCollector output, Reporter reporter)

              { + this.key = key; + this.value = value; + this.output = output; + this.reporter = reporter; + }

              +
              + /**
              + * Executes a Mapper.map call with the given Mapper and parameters.
              + * <p>
              + * This method is called from the thread-pool thread.
              + *
              + */
              + public void run() {
              + try

              { + // map pair to output + MultithreadedMapRunner.this.mapper.map(key, value, output, reporter); + }

              + catch (IOException ex) {
              + // If there is an IOException during the call it is set in an instance
              + // variable of the MultithreadedMapRunner from where it will be
              + // rethrown.
              + synchronized (MultithreadedMapRunner.this)

              Unknown macro: {+ if (MultithreadedMapRunner.this.ioException == null) { + MultithreadedMapRunner.this.ioException = ex; + }+ }

              + }
              + }
              + }
              +
              +}

        Show
        Alejandro Abdelnur added a comment - Following is the full 'svn diff' as requested. I'm also re-attaching the changed (per comments) MultithreadedMapRunner. Index: src/java/org/apache/hadoop/mapred/Task.java =================================================================== — src/java/org/apache/hadoop/mapred/Task.java (revision 486454) +++ src/java/org/apache/hadoop/mapred/Task.java (working copy) @@ -154,8 +154,10 @@ final Progress progress) throws IOException { return new Reporter() { public void setStatus(String status) throws IOException { progress.setStatus(status); progress(); + synchronized (this) { + progress.setStatus(status); + progress(); + } } public void progress() throws IOException { reportProgress(umbilical); Index: src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java =================================================================== src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java (revision 0) +++ src/java/org/apache/hadoop/mapred/lib/MultithreadedMapRunner.java (revision 0) @@ -0,0 +1,199 @@ +package org.apache.hadoop.mapred.lib; + +import org.apache.hadoop.util.ReflectionUtils; +import org.apache.hadoop.io.WritableComparable; +import org.apache.hadoop.io.Writable; +import org.apache.hadoop.mapred.MapRunnable; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.mapred.Mapper; +import org.apache.hadoop.mapred.RecordReader; +import org.apache.hadoop.mapred.OutputCollector; +import org.apache.hadoop.mapred.Reporter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Multithreaded implementation for @link org.apache.hadoop.mapred.MapRunnable. + * <p> + * It can be used instead of the default implementation, + * @link org.apache.hadoop.mapred.MapRunner, when the Map operation is not CPU + * bound in order to improve throughput. + * <p> + * Map implementations using this MapRunnable must be thread-safe. + * <p> + * The Map-Reduce job has to be configured to use this MapRunnable class (using + * the <b>mapred.map.runner.class</b> property) and + * the number of thread the thread-pool can use (using the + * <b>mapred.map.multithreadedrunner.threads</b> property). + * <p> + * + * @author Alejandro Abdelnur + */ +public class MultithreadedMapRunner implements MapRunnable { + private static final Log LOG = + LogFactory.getLog(MultithreadedMapRunner.class.getName()); + + private JobConf job; + private Mapper mapper; + private ExecutorService executorService; + private volatile IOException ioException; + + public void configure(JobConf job) Unknown macro: {+ int numberOfThreads =+ job.getInt("mapred.map.multithreadedrunner.threads", 10);+ if (LOG.isDebugEnabled()) { + LOG.debug("Configuring job " + job.getJobName() + + " to use " + numberOfThreads + " threads" ); + }++ this.job = job;+ this.mapper = (Mapper)ReflectionUtils.newInstance(job.getMapperClass(),+ job);++ // Creating a threadpool of the configured size to execute the Mapper+ // map method in parallel.+ executorService = Executors.newFixedThreadPool(numberOfThreads);+ } + + public void run(RecordReader input, OutputCollector output, + Reporter reporter) + throws IOException { + try { + // allocate key & value instances these objects will not be reused + // because execution of Mapper.map is not serialized. + WritableComparable key = input.createKey(); + Writable value = input.createValue(); + + while (input.next(key, value)) Unknown macro: {++ // Run Mapper.map execution asynchronously in a separate thread.+ // If threads are not available from the thread-pool this method+ // will block until there is a thread available.+ executorService.execute(+ new MapperInvokeRunable(key, value, output, reporter));++ // Checking if a Mapper.map within a Runnable has generated an+ // IOException. If so we rethrow it to force an abort of the Map+ // operation thus keeping the semantics of the default+ // implementation.+ if (ioException != null) { + throw ioException; + } + + // Allocate new key & value instances as mapper is running in parallel + key = input.createKey(); + value = input.createValue(); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Finished dispatching all Mappper.map calls, job " + + job.getJobName()); + } + + // Graceful shutdown of the Threadpool, it will let all scheduled + // Runnables to end. + executorService.shutdown(); + + try { + + // Now waiting for all Runnables to end. + while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Awaiting all running Mappper.map calls to finish, job " + + job.getJobName()); + } + + // Checking if a Mapper.map within a Runnable has generated an + // IOException. If so we rethrow it to force an abort of the Map + // operation thus keeping the semantics of the default + // implementation. + // NOTE: while Mapper.map dispatching has concluded there are still + // map calls in progress. + if (ioException != null) { + throw ioException; + } + } + + // Checking if a Mapper.map within a Runnable has generated an + // IOException. If so we rethrow it to force an abort of the Map + // operation thus keeping the semantics of the default + // implementation. + // NOTE: it could be that a map call has had an exception after the + // call for awaitTermination() returing true. And edge case but it + // could happen. + if (ioException != null) { + throw ioException; + }+ } + catch (IOException ioEx) { + // Forcing a shutdown of all thread of the threadpool and rethrowing + // the IOException + executorService.shutdownNow(); + throw ioEx; + } + catch (InterruptedException iEx) { + throw new IOException(iEx.getMessage()); + } + + } finally { + mapper.close(); + } + } + + + /** + * Runnable to execute a single Mapper.map call from a forked thread. + */ + private class MapperInvokeRunable implements Runnable { + private WritableComparable key; + private Writable value; + private OutputCollector output; + private Reporter reporter; + + /** + * Collecting all required parameters to execute a Mapper.map call. + * <p> + * + * @param key + * @param value + * @param output + * @param reporter + */ + public MapperInvokeRunable(WritableComparable key, Writable value, + OutputCollector output, Reporter reporter) { + this.key = key; + this.value = value; + this.output = output; + this.reporter = reporter; + } + + /** + * Executes a Mapper.map call with the given Mapper and parameters. + * <p> + * This method is called from the thread-pool thread. + * + */ + public void run() { + try { + // map pair to output + MultithreadedMapRunner.this.mapper.map(key, value, output, reporter); + } + catch (IOException ex) { + // If there is an IOException during the call it is set in an instance + // variable of the MultithreadedMapRunner from where it will be + // rethrown. + synchronized (MultithreadedMapRunner.this) Unknown macro: {+ if (MultithreadedMapRunner.this.ioException == null) { + MultithreadedMapRunner.this.ioException = ex; + }+ } + } + } + } + +}
        Hide
        Alejandro Abdelnur added a comment -

        Thxs.

        #1 Accept

        #2.a It is already being done, see the run() method of the MTMapperRunnable class.

        #2.b I would not use a list, 2 reasons: 1st not way to propagate more than one exception and 2nd that would mean a different semantics than the MapRunner.

        #3 Accept. Move/rename as you please

        #4 Accept. Will do in a few mins, IntelliJ optimized many imports here and there and I have many false diffs.

        Show
        Alejandro Abdelnur added a comment - Thxs. #1 Accept #2.a It is already being done, see the run() method of the MTMapperRunnable class. #2.b I would not use a list, 2 reasons: 1st not way to propagate more than one exception and 2nd that would mean a different semantics than the MapRunner. #3 Accept. Move/rename as you please #4 Accept. Will do in a few mins, IntelliJ optimized many imports here and there and I have many false diffs.
        Hide
        Owen O'Malley added a comment -

        It looks good. (Actually, this is exactly the use that MapRunnable was intended. Look at the Fetcher code in Nutch and it is basically doing very similar things.)

        1. ioException should be volatile since it is set/read in different threads without locks.
        2. I think that if ioException is already non-null, its value should not be changed. (Would a List<IOException> make more sense?)
        3. I think the class should be in the mapred.lib package instead of mapred.
        4. Please attach patches generated by "svn diff" rather than individual files.

        Show
        Owen O'Malley added a comment - It looks good. (Actually, this is exactly the use that MapRunnable was intended. Look at the Fetcher code in Nutch and it is basically doing very similar things.) 1. ioException should be volatile since it is set/read in different threads without locks. 2. I think that if ioException is already non-null, its value should not be changed. (Would a List<IOException> make more sense?) 3. I think the class should be in the mapred.lib package instead of mapred. 4. Please attach patches generated by "svn diff" rather than individual files.
        Hide
        Alejandro Abdelnur added a comment -

        Attachement for the MultithreadedMapRunner class

        Show
        Alejandro Abdelnur added a comment - Attachement for the MultithreadedMapRunner class

          People

          • Assignee:
            Doug Cutting
            Reporter:
            Alejandro Abdelnur
          • Votes:
            1 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development