Quiero usar a Streampara paralelizar el procesamiento de un conjunto heterogéneo de archivos JSON almacenados de forma remota de un número desconocido (el número de archivos no se conoce por adelantado). Los archivos pueden variar ampliamente en tamaño, desde 1 registro JSON por archivo hasta 100,000 registros en algunos otros archivos. Un registro JSON en este caso significa un objeto JSON autónomo representado como una línea en el archivo.
Realmente quiero usar Streams para esto, así que implementé esto Spliterator:
public abstract class JsonStreamSpliterator<METADATA, RECORD> extends AbstractSpliterator<RECORD> {
abstract protected JsonStreamSupport<METADATA> openInputStream(String path);
abstract protected RECORD parse(METADATA metadata, Map<String, Object> json);
private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.IMMUTABLE | Spliterator.DISTINCT | Spliterator.NONNULL;
private static final int MAX_BUFFER = 100;
private final Iterator<String> paths;
private JsonStreamSupport<METADATA> reader = null;
public JsonStreamSpliterator(Iterator<String> paths) {
this(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths);
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths) {
super(est, additionalCharacteristics);
this.paths = paths;
}
private JsonStreamSpliterator(long est, int additionalCharacteristics, Iterator<String> paths, String nextPath) {
this(est, additionalCharacteristics, paths);
open(nextPath);
}
@Override
public boolean tryAdvance(Consumer<? super RECORD> action) {
if(reader == null) {
String path = takeNextPath();
if(path != null) {
open(path);
}
else {
return false;
}
}
Map<String, Object> json = reader.readJsonLine();
if(json != null) {
RECORD item = parse(reader.getMetadata(), json);
action.accept(item);
return true;
}
else {
reader.close();
reader = null;
return tryAdvance(action);
}
}
private void open(String path) {
reader = openInputStream(path);
}
private String takeNextPath() {
synchronized(paths) {
if(paths.hasNext()) {
return paths.next();
}
}
return null;
}
@Override
public Spliterator<RECORD> trySplit() {
String nextPath = takeNextPath();
if(nextPath != null) {
return new JsonStreamSpliterator<METADATA,RECORD>(Long.MAX_VALUE, ADDITIONAL_CHARACTERISTICS, paths, nextPath) {
@Override
protected JsonStreamSupport<METADATA> openInputStream(String path) {
return JsonStreamSpliterator.this.openInputStream(path);
}
@Override
protected RECORD parse(METADATA metaData, Map<String,Object> json) {
return JsonStreamSpliterator.this.parse(metaData, json);
}
};
}
else {
List<RECORD> records = new ArrayList<RECORD>();
while(tryAdvance(records::add) && records.size() < MAX_BUFFER) {
// loop
}
if(records.size() != 0) {
return records.spliterator();
}
else {
return null;
}
}
}
}
El problema que tengo es que, si bien el Stream se paraleliza maravillosamente al principio, eventualmente el archivo más grande se deja procesar en un solo hilo. Creo que la causa proximal está bien documentada: el spliterator está "desequilibrado".
Más concretamente, parece que el trySplitmétodo no se llama después de cierto punto en el Stream.forEachciclo de vida del mismo, por lo que la lógica adicional para distribuir lotes pequeños al final trySplitrara vez se ejecuta.
Observe cómo todos los spliteradores devueltos por trySplit comparten el mismo pathsiterador. Pensé que esta era una forma realmente inteligente de equilibrar el trabajo en todos los spliteradores, pero no ha sido suficiente para lograr un paralelismo completo.
Me gustaría que el procesamiento paralelo proceda primero a través de los archivos, y luego, cuando todavía quedan pocos archivos grandes divididos, quiero paralelizar a través de fragmentos de los archivos restantes. Esa fue la intención del elsebloque al final de trySplit.
¿Hay alguna manera fácil / simple / canónica de solucionar este problema?
Long.MAX_VALUEcausa una división excesiva e innecesaria, mientras que cualquier estimación que no sea la Long.MAX_VALUEcausa de la división adicional se detiene, matando el paralelismo. Devolver una combinación de estimaciones precisas no parece llevar a ninguna optimización inteligente.
AbstractSpliteratorpero anulando, lo trySplit()cual es un mal combo para otra cosa que no sea Long.MAX_VALUE, ya que no está adaptando el tamaño estimado trySplit(). Después trySplit(), la estimación del tamaño debe reducirse por el número de elementos que se han dividido.