[ 
https://issues.apache.org/jira/browse/NUTCH-2455?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18136265#comment-18136265
 ] 

ASF GitHub Bot commented on NUTCH-2455:
---------------------------------------

lewismc commented on code in PR #888:
URL: https://github.com/apache/nutch/pull/888#discussion_r4438570485


##########
src/java/org/apache/nutch/crawl/Generator.java:
##########
@@ -148,35 +208,320 @@ public String toString() {
     }
   }
 
-  /** Selects entries due for fetch. */
+  /**
+   * Composite key for secondary sorting. Contains score and hostname.
+   * Used to ensure HostDb entries arrive before CrawlDb entries in the 
reducer.
+   *
+   * For HostDb entries: FloatTextPair(-Float.MAX_VALUE, hostname)
+   * For CrawlDb entries: FloatTextPair(score, "")
+   */
+  public static class FloatTextPair implements 
WritableComparable<FloatTextPair> {
+    public FloatWritable first;
+    public Text second;
+
+    public FloatTextPair() {
+      this.first = new FloatWritable();
+      this.second = new Text();
+    }
+
+    public FloatTextPair(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    public FloatTextPair(float first, String second) {
+      this.first = new FloatWritable(first);
+      this.second = new Text(second);
+    }
+
+    public FloatWritable getFirst() {
+      return first;
+    }
+
+    public void setFirst(FloatWritable first) {
+      this.first = first;
+    }
+
+    public Text getSecond() {
+      return second;
+    }
+
+    public void setSecond(Text second) {
+      this.second = second;
+    }
+
+    public void set(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    @Override
+    public int hashCode() {
+      return first.hashCode() * 163 + second.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof FloatTextPair) {
+        FloatTextPair tp = (FloatTextPair) obj;
+        return first.equals(tp.getFirst()) && second.equals(tp.getSecond());
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return first + "\t" + second;
+    }
+
+    @Override
+    public void readFields(DataInput in) throws IOException {
+      first.readFields(in);
+      second.readFields(in);
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+      first.write(out);
+      second.write(out);
+    }
+
+    @Override
+    public int compareTo(FloatTextPair tp) {
+      int cmp = first.compareTo(tp.getFirst());
+      if (cmp != 0)
+        return cmp;
+      return second.compareTo(tp.getSecond());
+    }
+  }
+
+  /**
+   * Comparator that ensures HostDb entries (with hostname in second field)
+   * sort before CrawlDb entries (with empty second field) for the same host.
+   * This enables the secondary sorting pattern for NUTCH-2455.
+   */
+  public static class ScoreHostKeyComparator extends WritableComparator {
+    protected ScoreHostKeyComparator() {
+      super(FloatTextPair.class, true);
+    }
+
+    @Override
+    public int compare(WritableComparable w1, WritableComparable w2) {
+      FloatTextPair key1 = (FloatTextPair) w1;
+      FloatTextPair key2 = (FloatTextPair) w2;
+
+      boolean isKey1HostDatum = key1.second.getLength() > 0;
+      boolean isKey2HostDatum = key2.second.getLength() > 0;
+
+      if (isKey1HostDatum && isKey2HostDatum) {
+        // Both are HostDb entries, sort by hostname
+        return key1.second.compareTo(key2.second);
+      } else {
+        if (isKey1HostDatum == isKey2HostDatum) {
+          // Both are CrawlDb entries, sort by score descending
+          return -1 * key1.first.compareTo(key2.first);
+        } else if (isKey1HostDatum) {
+          // HostDb entries come before CrawlDb entries
+          return -1;
+        } else {
+          return 1;
+        }
+      }
+    }
+  }
+
+  /**
+   * Mapper that reads HostDb and emits entries for secondary sorting.
+   * Uses a very low score (-Float.MAX_VALUE) to ensure HostDb entries
+   * sort before CrawlDb entries.
+   */
+  public static class HostDbReaderMapper
+      extends Mapper<Text, HostDatum, FloatTextPair, SelectorEntry> {
+
+    @Override
+    public void map(Text hostname, HostDatum value, Context context)
+        throws IOException, InterruptedException {
+      SelectorEntry hostDataSelector = new SelectorEntry();
+      hostDataSelector.hasHostDatum.set(true);
+      try {
+        hostDataSelector.hostdatum = (HostDatum) value.clone();
+      } catch (CloneNotSupportedException e) {
+        hostDataSelector.hostdatum = value;
+      }
+
+      // Use very low score and hostname to ensure HostDb entries
+      // sort before CrawlDb entries for the same host
+      context.write(new FloatTextPair(new FloatWritable(-Float.MAX_VALUE),
+          hostname), hostDataSelector);
+    }
+  }
+
+  /** Selects entries due for fetch. Partitions by host/domain/IP (with HostDb 
support). */
+  public static class SelectorWithHostDb extends Partitioner<FloatTextPair, 
Writable>
+      implements Configurable {
+
+    private final URLPartitioner partitioner = new URLPartitioner();
+    private final Map<String, Integer> partitionByHost = new HashMap<>();
+    private Configuration conf;
+    private int seed;
+    private int cachedReduceTasks = -1;
+    private String mode = URLPartitioner.PARTITION_MODE_HOST;
+
+    /**
+     * Partition by host / domain or IP.
+     * For HostDb entries (key.second is non-empty), partition by the hostname.
+     * For CrawlDb entries (key.second is empty), use the URLPartitioner.
+     *
+     * Note: When partition.url.mode is not "byHost", HostDb entries may not
+     * be correctly grouped with their corresponding CrawlDb entries. For best
+     * results, use "byHost" mode (the default) when using HostDb.
+     */
+    @Override
+    public int getPartition(FloatTextPair key, Writable value,
+        int numReduceTasks) {
+      SelectorEntry entry = (SelectorEntry) value;
+      // For HostDb entries, use the hostname from the key
+      // For CrawlDb entries, use the URL from the entry
+      if (key.second.getLength() > 0) {
+        // HostDb entry - partition by hostname
+        // This works best with "byHost" mode; other modes may cause
+        // HostDb entries to not be grouped with their CrawlDb entries
+        int hashCode = key.second.toString().hashCode();
+        hashCode ^= seed;
+        return (hashCode & Integer.MAX_VALUE) % numReduceTasks;
+      }
+      if (cachedReduceTasks != numReduceTasks) {
+        partitionByHost.clear();
+        cachedReduceTasks = numReduceTasks;
+      }
+      String host = rawHost(entry.url);
+      if (host != null) {
+        Integer cached = partitionByHost.get(host);
+        if (cached != null) {
+          return cached;
+        }
+      }
+      int part = partitioner.getPartition(entry.url, key.first, 
numReduceTasks);
+      if (host != null) {
+        partitionByHost.put(host, part);
+      }
+      return part;
+    }
+
+    /**
+     * Host used as the CrawlDb partition cache key. Returns null when the URL
+     * has no scheme or host, so those URLs keep the uncached partitioner path.
+     */
+    private static String rawHost(Text url) {
+      int length = url.getLength();
+      if (length <= 0) {
+        return null;
+      }
+      byte[] bytes = url.getBytes();
+      int scheme = -1;
+      for (int i = 0; i + 2 < length; i++) {
+        if (bytes[i] == ':' && bytes[i + 1] == '/' && bytes[i + 2] == '/') {
+          scheme = i;
+          break;
+        }
+      }
+      if (scheme < 0) {
+        return null;
+      }
+      int start = scheme + 3;
+      int authorityEnd = start;
+      int at = -1;
+      for (int i = start; i < length; i++) {
+        byte c = bytes[i];
+        if (c == '/' || c == '?' || c == '#') {
+          break;
+        }
+        if (c == '@') {
+          at = i;
+        }
+        authorityEnd = i + 1;
+      }
+      int hostStart = at >= 0 ? at + 1 : start;
+      int hostEnd;
+      if (hostStart < authorityEnd && bytes[hostStart] == '[') {
+        hostEnd = hostStart + 1;
+        while (hostEnd < authorityEnd && bytes[hostEnd] != ']') {
+          hostEnd++;
+        }
+        if (hostEnd < authorityEnd && bytes[hostEnd] == ']') {
+          hostEnd++;
+        }
+      } else {
+        hostEnd = hostStart;
+        while (hostEnd < authorityEnd && bytes[hostEnd] != ':') {
+          hostEnd++;
+        }
+      }
+      if (hostEnd <= hostStart) {
+        return null;
+      }
+      return new String(bytes, hostStart, hostEnd - hostStart,
+          StandardCharsets.UTF_8).toLowerCase(Locale.ROOT);
+    }
+
+    @Override
+    public Configuration getConf() {
+      return conf;
+    }
+
+    @Override
+    public void setConf(Configuration conf) {
+      this.conf = conf;
+      this.seed = conf.getInt("partition.url.seed", 0);
+      this.mode = conf.get(URLPartitioner.PARTITION_MODE_KEY,
+          URLPartitioner.PARTITION_MODE_HOST);
+      partitionByHost.clear();
+      cachedReduceTasks = -1;
+      partitioner.setConf(conf);
+
+      // Warn if mode is not byHost and hostdb is configured
+      String hostdb = conf.get(GENERATOR_HOSTDB);
+      if (hostdb != null && !mode.equals(URLPartitioner.PARTITION_MODE_HOST)) {
+        LOG.warn("HostDb is configured but partition.url.mode is '{}'. " +

Review Comment:
   Done. When `generate.hostdb` is set and `partition.url.mode` is not 
`byHost`, HostDb generation logs an error with the configured mode and forces 
`byHost` for this partitioner. The job configuration is left unchanged, so 
fetch and `generate.count.mode` keep the configured mode.
   
   Forcing `byHost` is what lets the HostDb row and its CrawlDb URLs share a 
reducer. Both now hash the lowercased host through 
`URLPartitioner.getPartitionForRoot`.



##########
src/java/org/apache/nutch/crawl/URLPartitioner.java:
##########
@@ -71,6 +71,33 @@ public Configuration getConf() {
     return conf;
   }
 
+  /**
+   * Returns the normalized URL root (host, domain, or IP) for the given URL 
key.
+   * This is used for partitioning URLs by their root component.
+   *
+   * @param key
+   *          The URL as a Text object.
+   * @return The normalized URL root based on the partition mode.
+   */
+  public String getNormalizedURLRoot(Text key) {

Review Comment:
   No. Nothing calls `getNormalizedURLRoot`. I removed it.



##########
src/java/org/apache/nutch/crawl/URLPartitioner.java:
##########
@@ -71,6 +71,33 @@ public Configuration getConf() {
     return conf;
   }
 
+  /**
+   * Returns the normalized URL root (host, domain, or IP) for the given URL 
key.
+   * This is used for partitioning URLs by their root component.
+   *
+   * @param key
+   *          The URL as a Text object.
+   * @return The normalized URL root based on the partition mode.
+   */
+  public String getNormalizedURLRoot(Text key) {
+    String urlString = key.toString();
+    URL url = null;
+    try {
+      urlString = normalizers.normalize(urlString, 
URLNormalizers.SCOPE_PARTITION);
+      url = new URL(urlString);
+    } catch (MalformedURLException e) {
+      LOG.warn("Malformed URL: '{}'", urlString);
+      return urlString;
+    }
+
+    try {
+      urlString = URLUtil.getUrlRootByMode(url, mode);

Review Comment:
   Agreed. The reducers already call `URLUtil.getUrlRootByMode` directly. 
`getPartition` now does the same after `SCOPE_PARTITION` normalization, and the 
wrapper method is gone.



##########
src/java/org/apache/nutch/crawl/URLPartitioner.java:
##########


Review Comment:
   Done. After normalization and parsing, `getPartition` takes 
`URLUtil.getUrlRootByMode(url, mode)` and hashes it with `getPartitionForRoot`. 
A malformed URL still hashes the string it already has. A `byIP` lookup that 
throws `UnknownHostException` still logs and hashes 0.



##########
src/java/org/apache/nutch/crawl/Generator.java:
##########
@@ -148,35 +208,320 @@ public String toString() {
     }
   }
 
-  /** Selects entries due for fetch. */
+  /**
+   * Composite key for secondary sorting. Contains score and hostname.
+   * Used to ensure HostDb entries arrive before CrawlDb entries in the 
reducer.
+   *
+   * For HostDb entries: FloatTextPair(-Float.MAX_VALUE, hostname)
+   * For CrawlDb entries: FloatTextPair(score, "")
+   */
+  public static class FloatTextPair implements 
WritableComparable<FloatTextPair> {
+    public FloatWritable first;
+    public Text second;
+
+    public FloatTextPair() {
+      this.first = new FloatWritable();
+      this.second = new Text();
+    }
+
+    public FloatTextPair(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    public FloatTextPair(float first, String second) {
+      this.first = new FloatWritable(first);
+      this.second = new Text(second);
+    }
+
+    public FloatWritable getFirst() {
+      return first;
+    }
+
+    public void setFirst(FloatWritable first) {
+      this.first = first;
+    }
+
+    public Text getSecond() {
+      return second;
+    }
+
+    public void setSecond(Text second) {
+      this.second = second;
+    }
+
+    public void set(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    @Override
+    public int hashCode() {
+      return first.hashCode() * 163 + second.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof FloatTextPair) {
+        FloatTextPair tp = (FloatTextPair) obj;
+        return first.equals(tp.getFirst()) && second.equals(tp.getSecond());
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return first + "\t" + second;
+    }
+
+    @Override
+    public void readFields(DataInput in) throws IOException {
+      first.readFields(in);
+      second.readFields(in);
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+      first.write(out);
+      second.write(out);
+    }
+
+    @Override
+    public int compareTo(FloatTextPair tp) {
+      int cmp = first.compareTo(tp.getFirst());
+      if (cmp != 0)
+        return cmp;
+      return second.compareTo(tp.getSecond());
+    }
+  }
+
+  /**
+   * Comparator that ensures HostDb entries (with hostname in second field)
+   * sort before CrawlDb entries (with empty second field) for the same host.
+   * This enables the secondary sorting pattern for NUTCH-2455.
+   */
+  public static class ScoreHostKeyComparator extends WritableComparator {
+    protected ScoreHostKeyComparator() {
+      super(FloatTextPair.class, true);
+    }
+
+    @Override
+    public int compare(WritableComparable w1, WritableComparable w2) {
+      FloatTextPair key1 = (FloatTextPair) w1;
+      FloatTextPair key2 = (FloatTextPair) w2;
+
+      boolean isKey1HostDatum = key1.second.getLength() > 0;
+      boolean isKey2HostDatum = key2.second.getLength() > 0;
+
+      if (isKey1HostDatum && isKey2HostDatum) {
+        // Both are HostDb entries, sort by hostname
+        return key1.second.compareTo(key2.second);
+      } else {
+        if (isKey1HostDatum == isKey2HostDatum) {
+          // Both are CrawlDb entries, sort by score descending
+          return -1 * key1.first.compareTo(key2.first);
+        } else if (isKey1HostDatum) {
+          // HostDb entries come before CrawlDb entries
+          return -1;
+        } else {
+          return 1;
+        }
+      }
+    }
+  }
+
+  /**
+   * Mapper that reads HostDb and emits entries for secondary sorting.
+   * Uses a very low score (-Float.MAX_VALUE) to ensure HostDb entries
+   * sort before CrawlDb entries.
+   */
+  public static class HostDbReaderMapper
+      extends Mapper<Text, HostDatum, FloatTextPair, SelectorEntry> {
+
+    @Override
+    public void map(Text hostname, HostDatum value, Context context)
+        throws IOException, InterruptedException {
+      SelectorEntry hostDataSelector = new SelectorEntry();
+      hostDataSelector.hasHostDatum.set(true);
+      try {
+        hostDataSelector.hostdatum = (HostDatum) value.clone();
+      } catch (CloneNotSupportedException e) {
+        hostDataSelector.hostdatum = value;
+      }
+
+      // Use very low score and hostname to ensure HostDb entries
+      // sort before CrawlDb entries for the same host
+      context.write(new FloatTextPair(new FloatWritable(-Float.MAX_VALUE),
+          hostname), hostDataSelector);
+    }
+  }
+
+  /** Selects entries due for fetch. Partitions by host/domain/IP (with HostDb 
support). */
+  public static class SelectorWithHostDb extends Partitioner<FloatTextPair, 
Writable>
+      implements Configurable {
+
+    private final URLPartitioner partitioner = new URLPartitioner();
+    private final Map<String, Integer> partitionByHost = new HashMap<>();
+    private Configuration conf;
+    private int seed;
+    private int cachedReduceTasks = -1;
+    private String mode = URLPartitioner.PARTITION_MODE_HOST;
+
+    /**
+     * Partition by host / domain or IP.
+     * For HostDb entries (key.second is non-empty), partition by the hostname.
+     * For CrawlDb entries (key.second is empty), use the URLPartitioner.
+     *
+     * Note: When partition.url.mode is not "byHost", HostDb entries may not
+     * be correctly grouped with their corresponding CrawlDb entries. For best
+     * results, use "byHost" mode (the default) when using HostDb.
+     */
+    @Override
+    public int getPartition(FloatTextPair key, Writable value,
+        int numReduceTasks) {
+      SelectorEntry entry = (SelectorEntry) value;
+      // For HostDb entries, use the hostname from the key
+      // For CrawlDb entries, use the URL from the entry
+      if (key.second.getLength() > 0) {
+        // HostDb entry - partition by hostname
+        // This works best with "byHost" mode; other modes may cause
+        // HostDb entries to not be grouped with their CrawlDb entries
+        int hashCode = key.second.toString().hashCode();
+        hashCode ^= seed;

Review Comment:
   Done. That hash now lives on `URLPartitioner.getPartitionForRoot`. The 
HostDb row calls it with the hostname lowercased, using the same 
`partition.url.seed` as the CrawlDb rows.



##########
src/java/org/apache/nutch/crawl/Generator.java:
##########
@@ -148,35 +208,320 @@ public String toString() {
     }
   }
 
-  /** Selects entries due for fetch. */
+  /**
+   * Composite key for secondary sorting. Contains score and hostname.
+   * Used to ensure HostDb entries arrive before CrawlDb entries in the 
reducer.
+   *
+   * For HostDb entries: FloatTextPair(-Float.MAX_VALUE, hostname)
+   * For CrawlDb entries: FloatTextPair(score, "")
+   */
+  public static class FloatTextPair implements 
WritableComparable<FloatTextPair> {
+    public FloatWritable first;
+    public Text second;
+
+    public FloatTextPair() {
+      this.first = new FloatWritable();
+      this.second = new Text();
+    }
+
+    public FloatTextPair(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    public FloatTextPair(float first, String second) {
+      this.first = new FloatWritable(first);
+      this.second = new Text(second);
+    }
+
+    public FloatWritable getFirst() {
+      return first;
+    }
+
+    public void setFirst(FloatWritable first) {
+      this.first = first;
+    }
+
+    public Text getSecond() {
+      return second;
+    }
+
+    public void setSecond(Text second) {
+      this.second = second;
+    }
+
+    public void set(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    @Override
+    public int hashCode() {
+      return first.hashCode() * 163 + second.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof FloatTextPair) {
+        FloatTextPair tp = (FloatTextPair) obj;
+        return first.equals(tp.getFirst()) && second.equals(tp.getSecond());
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return first + "\t" + second;
+    }
+
+    @Override
+    public void readFields(DataInput in) throws IOException {
+      first.readFields(in);
+      second.readFields(in);
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+      first.write(out);
+      second.write(out);
+    }
+
+    @Override
+    public int compareTo(FloatTextPair tp) {
+      int cmp = first.compareTo(tp.getFirst());
+      if (cmp != 0)
+        return cmp;
+      return second.compareTo(tp.getSecond());
+    }
+  }
+
+  /**
+   * Comparator that ensures HostDb entries (with hostname in second field)
+   * sort before CrawlDb entries (with empty second field) for the same host.
+   * This enables the secondary sorting pattern for NUTCH-2455.
+   */
+  public static class ScoreHostKeyComparator extends WritableComparator {
+    protected ScoreHostKeyComparator() {
+      super(FloatTextPair.class, true);
+    }
+
+    @Override
+    public int compare(WritableComparable w1, WritableComparable w2) {
+      FloatTextPair key1 = (FloatTextPair) w1;
+      FloatTextPair key2 = (FloatTextPair) w2;
+
+      boolean isKey1HostDatum = key1.second.getLength() > 0;
+      boolean isKey2HostDatum = key2.second.getLength() > 0;
+
+      if (isKey1HostDatum && isKey2HostDatum) {
+        // Both are HostDb entries, sort by hostname
+        return key1.second.compareTo(key2.second);
+      } else {
+        if (isKey1HostDatum == isKey2HostDatum) {
+          // Both are CrawlDb entries, sort by score descending
+          return -1 * key1.first.compareTo(key2.first);
+        } else if (isKey1HostDatum) {
+          // HostDb entries come before CrawlDb entries
+          return -1;
+        } else {
+          return 1;
+        }
+      }
+    }
+  }
+
+  /**
+   * Mapper that reads HostDb and emits entries for secondary sorting.
+   * Uses a very low score (-Float.MAX_VALUE) to ensure HostDb entries
+   * sort before CrawlDb entries.
+   */
+  public static class HostDbReaderMapper
+      extends Mapper<Text, HostDatum, FloatTextPair, SelectorEntry> {
+
+    @Override
+    public void map(Text hostname, HostDatum value, Context context)
+        throws IOException, InterruptedException {
+      SelectorEntry hostDataSelector = new SelectorEntry();
+      hostDataSelector.hasHostDatum.set(true);
+      try {
+        hostDataSelector.hostdatum = (HostDatum) value.clone();
+      } catch (CloneNotSupportedException e) {
+        hostDataSelector.hostdatum = value;
+      }
+
+      // Use very low score and hostname to ensure HostDb entries
+      // sort before CrawlDb entries for the same host
+      context.write(new FloatTextPair(new FloatWritable(-Float.MAX_VALUE),
+          hostname), hostDataSelector);
+    }
+  }
+
+  /** Selects entries due for fetch. Partitions by host/domain/IP (with HostDb 
support). */
+  public static class SelectorWithHostDb extends Partitioner<FloatTextPair, 
Writable>
+      implements Configurable {
+
+    private final URLPartitioner partitioner = new URLPartitioner();
+    private final Map<String, Integer> partitionByHost = new HashMap<>();
+    private Configuration conf;
+    private int seed;
+    private int cachedReduceTasks = -1;
+    private String mode = URLPartitioner.PARTITION_MODE_HOST;
+
+    /**
+     * Partition by host / domain or IP.
+     * For HostDb entries (key.second is non-empty), partition by the hostname.
+     * For CrawlDb entries (key.second is empty), use the URLPartitioner.
+     *
+     * Note: When partition.url.mode is not "byHost", HostDb entries may not
+     * be correctly grouped with their corresponding CrawlDb entries. For best
+     * results, use "byHost" mode (the default) when using HostDb.
+     */
+    @Override
+    public int getPartition(FloatTextPair key, Writable value,
+        int numReduceTasks) {
+      SelectorEntry entry = (SelectorEntry) value;
+      // For HostDb entries, use the hostname from the key
+      // For CrawlDb entries, use the URL from the entry
+      if (key.second.getLength() > 0) {
+        // HostDb entry - partition by hostname
+        // This works best with "byHost" mode; other modes may cause
+        // HostDb entries to not be grouped with their CrawlDb entries
+        int hashCode = key.second.toString().hashCode();
+        hashCode ^= seed;
+        return (hashCode & Integer.MAX_VALUE) % numReduceTasks;
+      }
+      if (cachedReduceTasks != numReduceTasks) {
+        partitionByHost.clear();
+        cachedReduceTasks = numReduceTasks;
+      }
+      String host = rawHost(entry.url);
+      if (host != null) {
+        Integer cached = partitionByHost.get(host);
+        if (cached != null) {
+          return cached;
+        }
+      }
+      int part = partitioner.getPartition(entry.url, key.first, 
numReduceTasks);

Review Comment:
   Done, with the method named `getPartitionForRoot`. `byHost` is now 
guaranteed for HostDb generation, and those CrawlDb rows do not run 
`SCOPE_PARTITION`. `rawHost` already has the host, and 
`SelectorWithHostDb.getPartition` passes it to `getPartitionForRoot`. A URL 
with no scheme or host still falls through to `URLPartitioner.getPartition`.



##########
src/java/org/apache/nutch/crawl/Generator.java:
##########
@@ -148,35 +208,320 @@ public String toString() {
     }
   }
 
-  /** Selects entries due for fetch. */
+  /**
+   * Composite key for secondary sorting. Contains score and hostname.
+   * Used to ensure HostDb entries arrive before CrawlDb entries in the 
reducer.
+   *
+   * For HostDb entries: FloatTextPair(-Float.MAX_VALUE, hostname)
+   * For CrawlDb entries: FloatTextPair(score, "")
+   */
+  public static class FloatTextPair implements 
WritableComparable<FloatTextPair> {
+    public FloatWritable first;
+    public Text second;
+
+    public FloatTextPair() {
+      this.first = new FloatWritable();
+      this.second = new Text();
+    }
+
+    public FloatTextPair(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    public FloatTextPair(float first, String second) {
+      this.first = new FloatWritable(first);
+      this.second = new Text(second);
+    }
+
+    public FloatWritable getFirst() {
+      return first;
+    }
+
+    public void setFirst(FloatWritable first) {
+      this.first = first;
+    }
+
+    public Text getSecond() {
+      return second;
+    }
+
+    public void setSecond(Text second) {
+      this.second = second;
+    }
+
+    public void set(FloatWritable first, Text second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    @Override
+    public int hashCode() {
+      return first.hashCode() * 163 + second.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof FloatTextPair) {
+        FloatTextPair tp = (FloatTextPair) obj;
+        return first.equals(tp.getFirst()) && second.equals(tp.getSecond());
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return first + "\t" + second;
+    }
+
+    @Override
+    public void readFields(DataInput in) throws IOException {
+      first.readFields(in);
+      second.readFields(in);
+    }
+
+    @Override
+    public void write(DataOutput out) throws IOException {
+      first.write(out);
+      second.write(out);
+    }
+
+    @Override
+    public int compareTo(FloatTextPair tp) {
+      int cmp = first.compareTo(tp.getFirst());
+      if (cmp != 0)
+        return cmp;
+      return second.compareTo(tp.getSecond());
+    }
+  }
+
+  /**
+   * Comparator that ensures HostDb entries (with hostname in second field)
+   * sort before CrawlDb entries (with empty second field) for the same host.
+   * This enables the secondary sorting pattern for NUTCH-2455.
+   */
+  public static class ScoreHostKeyComparator extends WritableComparator {
+    protected ScoreHostKeyComparator() {
+      super(FloatTextPair.class, true);
+    }
+
+    @Override
+    public int compare(WritableComparable w1, WritableComparable w2) {
+      FloatTextPair key1 = (FloatTextPair) w1;
+      FloatTextPair key2 = (FloatTextPair) w2;
+
+      boolean isKey1HostDatum = key1.second.getLength() > 0;
+      boolean isKey2HostDatum = key2.second.getLength() > 0;
+
+      if (isKey1HostDatum && isKey2HostDatum) {
+        // Both are HostDb entries, sort by hostname
+        return key1.second.compareTo(key2.second);
+      } else {
+        if (isKey1HostDatum == isKey2HostDatum) {
+          // Both are CrawlDb entries, sort by score descending
+          return -1 * key1.first.compareTo(key2.first);
+        } else if (isKey1HostDatum) {
+          // HostDb entries come before CrawlDb entries
+          return -1;
+        } else {
+          return 1;
+        }
+      }
+    }
+  }
+
+  /**
+   * Mapper that reads HostDb and emits entries for secondary sorting.
+   * Uses a very low score (-Float.MAX_VALUE) to ensure HostDb entries
+   * sort before CrawlDb entries.
+   */
+  public static class HostDbReaderMapper
+      extends Mapper<Text, HostDatum, FloatTextPair, SelectorEntry> {
+
+    @Override
+    public void map(Text hostname, HostDatum value, Context context)
+        throws IOException, InterruptedException {
+      SelectorEntry hostDataSelector = new SelectorEntry();
+      hostDataSelector.hasHostDatum.set(true);
+      try {
+        hostDataSelector.hostdatum = (HostDatum) value.clone();
+      } catch (CloneNotSupportedException e) {
+        hostDataSelector.hostdatum = value;
+      }
+
+      // Use very low score and hostname to ensure HostDb entries
+      // sort before CrawlDb entries for the same host
+      context.write(new FloatTextPair(new FloatWritable(-Float.MAX_VALUE),
+          hostname), hostDataSelector);
+    }
+  }
+
+  /** Selects entries due for fetch. Partitions by host/domain/IP (with HostDb 
support). */
+  public static class SelectorWithHostDb extends Partitioner<FloatTextPair, 
Writable>
+      implements Configurable {
+
+    private final URLPartitioner partitioner = new URLPartitioner();
+    private final Map<String, Integer> partitionByHost = new HashMap<>();
+    private Configuration conf;
+    private int seed;
+    private int cachedReduceTasks = -1;
+    private String mode = URLPartitioner.PARTITION_MODE_HOST;
+
+    /**
+     * Partition by host / domain or IP.
+     * For HostDb entries (key.second is non-empty), partition by the hostname.
+     * For CrawlDb entries (key.second is empty), use the URLPartitioner.
+     *
+     * Note: When partition.url.mode is not "byHost", HostDb entries may not
+     * be correctly grouped with their corresponding CrawlDb entries. For best
+     * results, use "byHost" mode (the default) when using HostDb.
+     */
+    @Override
+    public int getPartition(FloatTextPair key, Writable value,
+        int numReduceTasks) {
+      SelectorEntry entry = (SelectorEntry) value;
+      // For HostDb entries, use the hostname from the key
+      // For CrawlDb entries, use the URL from the entry
+      if (key.second.getLength() > 0) {
+        // HostDb entry - partition by hostname
+        // This works best with "byHost" mode; other modes may cause
+        // HostDb entries to not be grouped with their CrawlDb entries
+        int hashCode = key.second.toString().hashCode();
+        hashCode ^= seed;
+        return (hashCode & Integer.MAX_VALUE) % numReduceTasks;
+      }
+      if (cachedReduceTasks != numReduceTasks) {
+        partitionByHost.clear();
+        cachedReduceTasks = numReduceTasks;
+      }
+      String host = rawHost(entry.url);
+      if (host != null) {
+        Integer cached = partitionByHost.get(host);
+        if (cached != null) {
+          return cached;
+        }
+      }
+      int part = partitioner.getPartition(entry.url, key.first, 
numReduceTasks);

Review Comment:
   Agreed, and the cache is gone. It existed because the 50M HostDb profile 
showed `URLNormalizers.normalize` inside `SelectorWithHostDb.getPartition` 
(about 4% of CPU and about 22% of allocation). The map stored 
`URLPartitioner`'s int so later URLs on the same host skipped that work. 
Skipping normalization on this path removes the work, so the map is 
unnecessary. The no-HostDb selector is unchanged and still calls 
`URLPartitioner` for every URL.





> Use secondary sorting for memory-efficient HostDb integration in Generator
> --------------------------------------------------------------------------
>
>                 Key: NUTCH-2455
>                 URL: https://issues.apache.org/jira/browse/NUTCH-2455
>             Project: Nutch
>          Issue Type: Improvement
>          Components: generator
>    Affects Versions: 1.13
>            Reporter: Markus Jelsma
>            Assignee: Lewis John McGibbney
>            Priority: Major
>             Fix For: 1.24
>
>         Attachments: NUTCH-2455.patch
>
>
> Citing Sebastian at NUTCH-2420:
> ??The correct solution would be to use <host,score> pairs as keys in the 
> Selector job, with a partitioner and secondary sorting so that all keys with 
> same host end up in the same call of the reducer. If values can also hold a 
> HostDb entry and the sort comparator guarantees that the HostDb entry 
> (entries if partitioned by domain or IP) comes in front of all CrawlDb 
> entries. But that would be a substantial improvement...??



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to