Complete Spark integration test suite

This commit is contained in:
chrislu
2025-11-22 12:23:48 -08:00
parent a428aeb5d7
commit 89a6d42cee
14 changed files with 1942 additions and 0 deletions
@@ -0,0 +1,143 @@
package seaweed.spark;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SaveMode;
import org.apache.spark.sql.SparkSession;
/**
* Example Spark application demonstrating SeaweedFS integration.
*
* This can be submitted to a Spark cluster using spark-submit.
*
* Example usage:
* spark-submit \
* --class seaweed.spark.SparkSeaweedFSExample \
* --master local[2] \
* --conf spark.hadoop.fs.seaweedfs.impl=seaweed.hdfs.SeaweedFileSystem \
* --conf spark.hadoop.fs.seaweed.filer.host=localhost \
* --conf spark.hadoop.fs.seaweed.filer.port=8888 \
* --conf spark.hadoop.fs.seaweed.filer.port.grpc=18888 \
* target/seaweedfs-spark-integration-tests-1.0-SNAPSHOT.jar \
* seaweedfs://localhost:8888/output
*/
public class SparkSeaweedFSExample {
public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: SparkSeaweedFSExample <output-path>");
System.err.println("Example: seaweedfs://localhost:8888/spark-output");
System.exit(1);
}
String outputPath = args[0];
// Create Spark session
SparkSession spark = SparkSession.builder()
.appName("SeaweedFS Spark Example")
.getOrCreate();
try {
System.out.println("=== SeaweedFS Spark Integration Example ===\n");
// Example 1: Generate data and write to SeaweedFS
System.out.println("1. Generating sample data...");
Dataset<Row> data = spark.range(0, 1000)
.selectExpr(
"id",
"id * 2 as doubled",
"CAST(rand() * 100 AS INT) as random_value"
);
System.out.println(" Generated " + data.count() + " rows");
data.show(5);
// Write as Parquet
String parquetPath = outputPath + "/data.parquet";
System.out.println("\n2. Writing data to SeaweedFS as Parquet...");
System.out.println(" Path: " + parquetPath);
data.write()
.mode(SaveMode.Overwrite)
.parquet(parquetPath);
System.out.println(" ✓ Write completed");
// Read back and verify
System.out.println("\n3. Reading data back from SeaweedFS...");
Dataset<Row> readData = spark.read().parquet(parquetPath);
System.out.println(" Read " + readData.count() + " rows");
// Perform aggregation
System.out.println("\n4. Performing aggregation...");
Dataset<Row> stats = readData.selectExpr(
"COUNT(*) as count",
"AVG(random_value) as avg_random",
"MAX(doubled) as max_doubled"
);
stats.show();
// Write aggregation results
String statsPath = outputPath + "/stats.parquet";
System.out.println("5. Writing stats to: " + statsPath);
stats.write()
.mode(SaveMode.Overwrite)
.parquet(statsPath);
// Create a partitioned dataset
System.out.println("\n6. Creating partitioned dataset...");
Dataset<Row> partitionedData = data.selectExpr(
"*",
"CAST(id % 10 AS INT) as partition_key"
);
String partitionedPath = outputPath + "/partitioned.parquet";
System.out.println(" Path: " + partitionedPath);
partitionedData.write()
.mode(SaveMode.Overwrite)
.partitionBy("partition_key")
.parquet(partitionedPath);
System.out.println(" ✓ Partitioned write completed");
// Read specific partition
System.out.println("\n7. Reading specific partition (partition_key=0)...");
Dataset<Row> partition0 = spark.read()
.parquet(partitionedPath)
.filter("partition_key = 0");
System.out.println(" Partition 0 contains " + partition0.count() + " rows");
partition0.show(5);
// SQL example
System.out.println("\n8. Using Spark SQL...");
readData.createOrReplaceTempView("seaweedfs_data");
Dataset<Row> sqlResult = spark.sql(
"SELECT " +
" CAST(id / 100 AS INT) as bucket, " +
" COUNT(*) as count, " +
" AVG(random_value) as avg_random " +
"FROM seaweedfs_data " +
"GROUP BY CAST(id / 100 AS INT) " +
"ORDER BY bucket"
);
System.out.println(" Bucketed statistics:");
sqlResult.show();
System.out.println("\n=== Example completed successfully! ===");
System.out.println("Output location: " + outputPath);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
} finally {
spark.stop();
}
}
}
@@ -0,0 +1,216 @@
package seaweed.spark;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SaveMode;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* Integration tests for Spark read/write operations with SeaweedFS.
*/
public class SparkReadWriteTest extends SparkTestBase {
@Test
public void testWriteAndReadParquet() {
skipIfTestsDisabled();
// Create test data
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
Dataset<Row> df = spark.createDataFrame(people, Person.class);
// Write to SeaweedFS
String outputPath = getTestPath("people.parquet");
df.write().mode(SaveMode.Overwrite).parquet(outputPath);
// Read back from SeaweedFS
Dataset<Row> readDf = spark.read().parquet(outputPath);
// Verify
assertEquals(3, readDf.count());
assertEquals(2, readDf.columns().length);
List<Row> results = readDf.collectAsList();
assertTrue(results.stream().anyMatch(r -> "Alice".equals(r.getAs("name")) && (Integer)r.getAs("age") == 30));
assertTrue(results.stream().anyMatch(r -> "Bob".equals(r.getAs("name")) && (Integer)r.getAs("age") == 25));
assertTrue(results.stream().anyMatch(r -> "Charlie".equals(r.getAs("name")) && (Integer)r.getAs("age") == 35));
}
@Test
public void testWriteAndReadCSV() {
skipIfTestsDisabled();
// Create test data
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25)
);
Dataset<Row> df = spark.createDataFrame(people, Person.class);
// Write to SeaweedFS as CSV
String outputPath = getTestPath("people.csv");
df.write().mode(SaveMode.Overwrite).option("header", "true").csv(outputPath);
// Read back from SeaweedFS
Dataset<Row> readDf = spark.read().option("header", "true").option("inferSchema", "true").csv(outputPath);
// Verify
assertEquals(2, readDf.count());
assertEquals(2, readDf.columns().length);
}
@Test
public void testWriteAndReadJSON() {
skipIfTestsDisabled();
// Create test data
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
Dataset<Row> df = spark.createDataFrame(people, Person.class);
// Write to SeaweedFS as JSON
String outputPath = getTestPath("people.json");
df.write().mode(SaveMode.Overwrite).json(outputPath);
// Read back from SeaweedFS
Dataset<Row> readDf = spark.read().json(outputPath);
// Verify
assertEquals(3, readDf.count());
assertEquals(2, readDf.columns().length);
}
@Test
public void testWritePartitionedData() {
skipIfTestsDisabled();
// Create test data with multiple years
List<PersonWithYear> people = Arrays.asList(
new PersonWithYear("Alice", 30, 2020),
new PersonWithYear("Bob", 25, 2021),
new PersonWithYear("Charlie", 35, 2020),
new PersonWithYear("David", 28, 2021)
);
Dataset<Row> df = spark.createDataFrame(people, PersonWithYear.class);
// Write partitioned data to SeaweedFS
String outputPath = getTestPath("people_partitioned");
df.write().mode(SaveMode.Overwrite).partitionBy("year").parquet(outputPath);
// Read back from SeaweedFS
Dataset<Row> readDf = spark.read().parquet(outputPath);
// Verify
assertEquals(4, readDf.count());
// Verify partition filtering works
Dataset<Row> filtered2020 = readDf.filter("year = 2020");
assertEquals(2, filtered2020.count());
Dataset<Row> filtered2021 = readDf.filter("year = 2021");
assertEquals(2, filtered2021.count());
}
@Test
public void testAppendMode() {
skipIfTestsDisabled();
String outputPath = getTestPath("people_append.parquet");
// Write first batch
List<Person> batch1 = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25)
);
Dataset<Row> df1 = spark.createDataFrame(batch1, Person.class);
df1.write().mode(SaveMode.Overwrite).parquet(outputPath);
// Append second batch
List<Person> batch2 = Arrays.asList(
new Person("Charlie", 35),
new Person("David", 28)
);
Dataset<Row> df2 = spark.createDataFrame(batch2, Person.class);
df2.write().mode(SaveMode.Append).parquet(outputPath);
// Read back and verify
Dataset<Row> readDf = spark.read().parquet(outputPath);
assertEquals(4, readDf.count());
}
@Test
public void testLargeDataset() {
skipIfTestsDisabled();
// Create a larger dataset
Dataset<Row> largeDf = spark.range(0, 10000)
.selectExpr("id as value", "id * 2 as doubled");
String outputPath = getTestPath("large_dataset.parquet");
largeDf.write().mode(SaveMode.Overwrite).parquet(outputPath);
// Read back and verify
Dataset<Row> readDf = spark.read().parquet(outputPath);
assertEquals(10000, readDf.count());
// Verify some data
Row firstRow = readDf.first();
assertEquals(0L, firstRow.getLong(0));
assertEquals(0L, firstRow.getLong(1));
}
// Test data classes
public static class Person implements java.io.Serializable {
private String name;
private int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
public static class PersonWithYear implements java.io.Serializable {
private String name;
private int age;
private int year;
public PersonWithYear() {}
public PersonWithYear(String name, int age, int year) {
this.name = name;
this.age = age;
this.year = year;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getYear() { return year; }
public void setYear(int year) { this.year = year; }
}
}
@@ -0,0 +1,236 @@
package seaweed.spark;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SaveMode;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* Integration tests for Spark SQL operations with SeaweedFS.
*/
public class SparkSQLTest extends SparkTestBase {
@Test
public void testCreateTableAndQuery() {
skipIfTestsDisabled();
// Create test data
List<Employee> employees = Arrays.asList(
new Employee(1, "Alice", "Engineering", 100000),
new Employee(2, "Bob", "Sales", 80000),
new Employee(3, "Charlie", "Engineering", 120000),
new Employee(4, "David", "Sales", 75000)
);
Dataset<Row> df = spark.createDataFrame(employees, Employee.class);
// Write to SeaweedFS
String tablePath = getTestPath("employees");
df.write().mode(SaveMode.Overwrite).parquet(tablePath);
// Create temporary view
Dataset<Row> employeesDf = spark.read().parquet(tablePath);
employeesDf.createOrReplaceTempView("employees");
// Run SQL queries
Dataset<Row> engineeringEmployees = spark.sql(
"SELECT name, salary FROM employees WHERE department = 'Engineering'"
);
assertEquals(2, engineeringEmployees.count());
Dataset<Row> highPaidEmployees = spark.sql(
"SELECT name, salary FROM employees WHERE salary > 90000"
);
assertEquals(2, highPaidEmployees.count());
}
@Test
public void testAggregationQueries() {
skipIfTestsDisabled();
// Create sales data
List<Sale> sales = Arrays.asList(
new Sale("2024-01", "Product A", 100),
new Sale("2024-01", "Product B", 150),
new Sale("2024-02", "Product A", 120),
new Sale("2024-02", "Product B", 180),
new Sale("2024-03", "Product A", 110)
);
Dataset<Row> df = spark.createDataFrame(sales, Sale.class);
// Write to SeaweedFS
String tablePath = getTestPath("sales");
df.write().mode(SaveMode.Overwrite).parquet(tablePath);
// Create temporary view
Dataset<Row> salesDf = spark.read().parquet(tablePath);
salesDf.createOrReplaceTempView("sales");
// Aggregate query
Dataset<Row> monthlySales = spark.sql(
"SELECT month, SUM(amount) as total FROM sales GROUP BY month ORDER BY month"
);
List<Row> results = monthlySales.collectAsList();
assertEquals(3, results.size());
assertEquals("2024-01", results.get(0).getString(0));
assertEquals(250, results.get(0).getLong(1));
}
@Test
public void testJoinOperations() {
skipIfTestsDisabled();
// Create employee data
List<Employee> employees = Arrays.asList(
new Employee(1, "Alice", "Engineering", 100000),
new Employee(2, "Bob", "Sales", 80000)
);
// Create department data
List<Department> departments = Arrays.asList(
new Department("Engineering", "Building Products"),
new Department("Sales", "Selling Products")
);
Dataset<Row> empDf = spark.createDataFrame(employees, Employee.class);
Dataset<Row> deptDf = spark.createDataFrame(departments, Department.class);
// Write to SeaweedFS
String empPath = getTestPath("employees_join");
String deptPath = getTestPath("departments_join");
empDf.write().mode(SaveMode.Overwrite).parquet(empPath);
deptDf.write().mode(SaveMode.Overwrite).parquet(deptPath);
// Read back and create views
spark.read().parquet(empPath).createOrReplaceTempView("emp");
spark.read().parquet(deptPath).createOrReplaceTempView("dept");
// Join query
Dataset<Row> joined = spark.sql(
"SELECT e.name, e.salary, d.description " +
"FROM emp e JOIN dept d ON e.department = d.name"
);
assertEquals(2, joined.count());
List<Row> results = joined.collectAsList();
assertTrue(results.stream().anyMatch(r ->
"Alice".equals(r.getString(0)) && "Building Products".equals(r.getString(2))
));
}
@Test
public void testWindowFunctions() {
skipIfTestsDisabled();
// Create employee data with salaries
List<Employee> employees = Arrays.asList(
new Employee(1, "Alice", "Engineering", 100000),
new Employee(2, "Bob", "Engineering", 120000),
new Employee(3, "Charlie", "Sales", 80000),
new Employee(4, "David", "Sales", 90000)
);
Dataset<Row> df = spark.createDataFrame(employees, Employee.class);
String tablePath = getTestPath("employees_window");
df.write().mode(SaveMode.Overwrite).parquet(tablePath);
Dataset<Row> employeesDf = spark.read().parquet(tablePath);
employeesDf.createOrReplaceTempView("employees_ranked");
// Window function query - rank employees by salary within department
Dataset<Row> ranked = spark.sql(
"SELECT name, department, salary, " +
"RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank " +
"FROM employees_ranked"
);
assertEquals(4, ranked.count());
// Verify Bob has rank 1 in Engineering (highest salary)
List<Row> results = ranked.collectAsList();
Row bobRow = results.stream()
.filter(r -> "Bob".equals(r.getString(0)))
.findFirst()
.orElse(null);
assertNotNull(bobRow);
assertEquals(1, bobRow.getInt(3));
}
// Test data classes
public static class Employee implements java.io.Serializable {
private int id;
private String name;
private String department;
private int salary;
public Employee() {}
public Employee(int id, String name, String department, int salary) {
this.id = id;
this.name = name;
this.department = department;
this.salary = salary;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public int getSalary() { return salary; }
public void setSalary(int salary) { this.salary = salary; }
}
public static class Sale implements java.io.Serializable {
private String month;
private String product;
private int amount;
public Sale() {}
public Sale(String month, String product, int amount) {
this.month = month;
this.product = product;
this.amount = amount;
}
public String getMonth() { return month; }
public void setMonth(String month) { this.month = month; }
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
public int getAmount() { return amount; }
public void setAmount(int amount) { this.amount = amount; }
}
public static class Department implements java.io.Serializable {
private String name;
private String description;
public Department() {}
public Department(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}
}
@@ -0,0 +1,126 @@
package seaweed.spark;
import org.apache.hadoop.conf.Configuration;
import org.apache.spark.SparkConf;
import org.apache.spark.sql.SparkSession;
import org.junit.After;
import org.junit.Before;
import java.io.IOException;
/**
* Base class for Spark integration tests with SeaweedFS.
*
* These tests require a running SeaweedFS cluster.
* Set environment variable SEAWEEDFS_TEST_ENABLED=true to enable these tests.
*/
public abstract class SparkTestBase {
protected SparkSession spark;
protected static final String TEST_ROOT = "/test-spark";
protected static final boolean TESTS_ENABLED =
"true".equalsIgnoreCase(System.getenv("SEAWEEDFS_TEST_ENABLED"));
// SeaweedFS connection settings
protected static final String SEAWEEDFS_HOST =
System.getenv().getOrDefault("SEAWEEDFS_FILER_HOST", "localhost");
protected static final String SEAWEEDFS_PORT =
System.getenv().getOrDefault("SEAWEEDFS_FILER_PORT", "8888");
protected static final String SEAWEEDFS_GRPC_PORT =
System.getenv().getOrDefault("SEAWEEDFS_FILER_GRPC_PORT", "18888");
@Before
public void setUpSpark() throws IOException {
if (!TESTS_ENABLED) {
return;
}
SparkConf sparkConf = new SparkConf()
.setAppName("SeaweedFS Integration Test")
.setMaster("local[1]") // Single thread to avoid concurrent gRPC issues
.set("spark.driver.host", "localhost")
.set("spark.sql.warehouse.dir", getSeaweedFSPath("/spark-warehouse"))
// SeaweedFS configuration
.set("spark.hadoop.fs.defaultFS", String.format("seaweedfs://%s:%s", SEAWEEDFS_HOST, SEAWEEDFS_PORT))
.set("spark.hadoop.fs.seaweedfs.impl", "seaweed.hdfs.SeaweedFileSystem")
.set("spark.hadoop.fs.seaweed.impl", "seaweed.hdfs.SeaweedFileSystem")
.set("spark.hadoop.fs.seaweed.filer.host", SEAWEEDFS_HOST)
.set("spark.hadoop.fs.seaweed.filer.port", SEAWEEDFS_PORT)
.set("spark.hadoop.fs.seaweed.filer.port.grpc", SEAWEEDFS_GRPC_PORT)
.set("spark.hadoop.fs.AbstractFileSystem.seaweedfs.impl", "seaweed.hdfs.SeaweedAbstractFileSystem")
// Set replication to empty string to use filer default
.set("spark.hadoop.fs.seaweed.replication", "")
// Smaller buffer to reduce load
.set("spark.hadoop.fs.seaweed.buffer.size", "1048576") // 1MB
// Reduce parallelism
.set("spark.default.parallelism", "1")
.set("spark.sql.shuffle.partitions", "1")
// Simpler output committer
.set("spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version", "2")
.set("spark.sql.sources.commitProtocolClass", "org.apache.spark.sql.execution.datasources.SQLHadoopMapReduceCommitProtocol")
// Disable speculative execution to reduce load
.set("spark.speculation", "false");
spark = SparkSession.builder()
.config(sparkConf)
.getOrCreate();
// Clean up test directory
cleanupTestDirectory();
}
@After
public void tearDownSpark() {
if (!TESTS_ENABLED || spark == null) {
return;
}
try {
// Try to cleanup but don't fail if it doesn't work
cleanupTestDirectory();
} catch (Exception e) {
System.err.println("Cleanup failed: " + e.getMessage());
} finally {
try {
spark.stop();
} catch (Exception e) {
System.err.println("Spark stop failed: " + e.getMessage());
}
spark = null;
}
}
protected String getSeaweedFSPath(String path) {
return String.format("seaweedfs://%s:%s%s", SEAWEEDFS_HOST, SEAWEEDFS_PORT, path);
}
protected String getTestPath(String subPath) {
return getSeaweedFSPath(TEST_ROOT + "/" + subPath);
}
private void cleanupTestDirectory() {
if (spark != null) {
try {
Configuration conf = spark.sparkContext().hadoopConfiguration();
org.apache.hadoop.fs.FileSystem fs = org.apache.hadoop.fs.FileSystem.get(
java.net.URI.create(getSeaweedFSPath("/")), conf);
org.apache.hadoop.fs.Path testPath = new org.apache.hadoop.fs.Path(TEST_ROOT);
if (fs.exists(testPath)) {
fs.delete(testPath, true);
}
} catch (Exception e) {
// Suppress cleanup errors - they shouldn't fail tests
// Common in distributed systems with eventual consistency
System.err.println("Warning: cleanup failed (non-critical): " + e.getMessage());
}
}
}
protected void skipIfTestsDisabled() {
if (!TESTS_ENABLED) {
System.out.println("Skipping test - SEAWEEDFS_TEST_ENABLED not set");
org.junit.Assume.assumeTrue("SEAWEEDFS_TEST_ENABLED not set", false);
}
}
}
@@ -0,0 +1,18 @@
# Set root logger level
log4j.rootLogger=WARN, console
# Console appender
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
# Set log levels for specific packages
log4j.logger.org.apache.spark=WARN
log4j.logger.org.apache.hadoop=WARN
log4j.logger.seaweed=INFO
# Suppress unnecessary warnings
log4j.logger.org.apache.spark.util.Utils=ERROR
log4j.logger.org.apache.hadoop.util.NativeCodeLoader=ERROR