Auto Added by WPeMatico

Kotlin API for Apache Spark v1.2: UDTs, UDFs, RDDs, Compatibility, and More!

Hi everyone, Jolan here, with my first actual blog post! It’s been a couple of months since the last release of the Kotlin API for Apache Spark and we feel like all of the exciting changes since then are worth another announcement.

Let me remind you what the Kotlin API for Apache Spark is and why it was created. Apache Spark is a framework for distributed computations, which data engineers usually use to solve different tasks, like in the ETL process. It supports multiple languages straight out of the box, including Java, Scala, Python, and R. We at JetBrains are committed to supporting one more language for Apache Spark – Kotlin. We believe it can combine multiple pros from other language APIs while avoiding their cons.

If you don’t want to read about it and just want to try it out – here is the link to the repo.

Kotlin Spark API on GitHub

Otherwise, let’s begin!

UDTs & UDFs

User-defined types (or UDTs) are Spark’s go-to method for allowing any instance of a data-containing class to be stored in and read from a Dataset. One of the extensions for Spark that we now fully support which uses these types is Mllib, a library that aims to make practical machine learning scalable and easy. An example using Mllib can be found here (in Datalore). To define your own UDTs, here’s another example (in Datalore).

While it’s generally recommended to use Spark’s column functions for your Dataset- or SQL operations, sometimes a User-defined function (UDF) or User-defined aggregate function (UDAF) can provide extra flexibility. While the Kotlin Spark API did contain basic UDF support, we have greatly expanded that support. If you want, you can jump straight to all examples (in Datalore).

Similar to Spark’s API, you can create a UDF or UDAF and register it to make it executable from SQL. You can follow the original API, but you can also use some neat Kotlin tricks to make it a bit easier:

withSpark {
    val plusOne by udf { x: Int -> x + 1 }
    plusOne.register()

    spark.sql("SELECT plusOne(5)").showDS()
    //    +---------------+
    //    |     plusOne(5)|
    //    +---------------+
    //    |              6|
    //    +---------------+

}

In contrast to Spark’s API, the created UDFs are now also typed. This means that when you directly invoke a UDF instance using Dataset.select() calls with the correctly typed TypedColumns, the UDF will return the correct types too:

data class User(val name: String, val age: Int)

withSpark {
    val toString by udf { x: Int -> x.toString() }
    
    val ds = dsOf(User(...), ...) // Dataset<User>

    val newDs = ds.select(
       col(User::name), 
       toString(col(User::age)),
    ) // Dataset<Tuple2<String, String>> 
}

Finally, we added simple Vararg-UDFs, which do not even exist in the Scala API. This allows a UDF to accept any number of same-type arguments and works both from SQL and the Dataset invocations.

(Note, the following example also works when you define a UDF the “normal” way using an array of any type as a single argument.)

fun sumOf(vararg double: Double): Double = double.sum() 

withSpark {
    udf.register(::sumOf)
    
    spark.sql("SELECT sumOf(1.0, 2.0, 3.0), sumOf(5.0)").showDS()
    //    +--------------------+----------+
    //    |sumOf(1.0, 2.0, 3.0)|sumOf(5.0)|
    //    +--------------------+----------+
    //    |                 6.0|       5.0|
    //    +--------------------+----------+

}

We strongly encourage you to take a look at the examples (in Datalore) since they go over everything that’s possible with the new UDFs, and most importantly, we encourage you to play around with them and let us know what you think!

RDDs

Resilient Distributed Datasets (RDDs) are the backbone of Apache Spark. While in most cases typed Datasets are preferred, RDDs still have a special place thanks to their flexibility and speed. They are also still used under the hood of Datasets. While the Java API of RDDs is perfectly usable from Kotlin, we thought we could port over some of the niceties from the Scala API.

Similar to Scala, when you’re working with an RDD with the type Tuple2, you can utilize all kinds of handy key / value functions, like aggregateByKey(), reduceByKey(), groupByKey(), filterByRange(), and values() – no mapToPair necessary!

This also holds true for RRDs with a numeric type. Whereas previously you’d need to mapToDouble, you can now call mean(), sum(), stdev()max(), etc. directly.

Finally, we added some Kotlin-esque creation functions surrounding RDDs, such as rddOf()List.toRDD(), and RDD.toDS().
For an extensive example surrounding generating unique groups of indices using RDDs, look no further (in Datalore).

Jupyter

We are excited to see people prototyping and preprocessing their data using Spark from Jupyter notebooks in Kotlin! We further enhanced support for this by adding some essential features:

While adding %use spark is usually enough to get you going, sometimes it’s essential to change some of the properties Spark uses. This is why from release v1.2 (and the Kotlin Jupyter kernel v0.11.0-134 and up), you can now define Spark properties directly in the use-magic!

For example:  %use spark(spark=3.3.0, scala=2.12, spark.master=local[4]).

Display options were also overhauled; You can now set them using a nice little DSL:

sparkProperties {
    displayLimit = 2
    displayTruncate = -1
}

This specific example will limit the number of rows displayed to only 2 and remove the truncation of text in cells entirely. These properties can also be provided in the use-magic.
Check Datalore for an example!

Compatibility

The largest changes for this release happened under the hood. We migrated the build system from Maven to Gradle and designed a new building method so that we can (as of right now) build 14 different versions of the Kotlin Spark API at once!

“Why 14 versions?” you might ask. Well… it’s complicated… But let me attempt to explain. You can also skip this and just jump to the new naming scheme below.

While most projects using Spark operate just fine with a patch-version change, the Kotlin Spark API works by replacing a core file of Spark. This file only operates well with the patch version it was created for, thanks to Spark using reflection to call parts of it.

Before, we would push releases just for each latest minor version of Spark (e.g. 3.0.3, 3.1.3, and 3.2.1). This worked fine, as long as you used the same exact Spark version. However, not everyone has the luxury of having the latest patch version of Spark, or even the same Scala version, especially when bound to an external server environment.

To ensure all of our users are able to use the latest version of the Kotlin Spark API, no matter their setup, we now build our API for all patch versions and all core Scala versions of Apache Spark from 3.0.0+, as found on Maven Central, using a preprocessor and lots of tests to solve the compatibility issues found in between versions.

The new naming scheme for the packages will now contain both the Apache Spark patch version, as well as the Scala core version:

[name]_[Apache Spark version]_[Scala core version]:[Kotlin for Apache Spark API version]

The only exception to this is scala-tuples-in-kotlin_[Scala core version]:[Kotlin for Apache Spark API version], which is independent of Spark and can be used on its own without Spark in any of your Kotlin / Scala hybrid projects!

The new release scheme looks as follows:

Apache Spark Scala Kotlin Spark API
3.3.0 2.13 kotlin-spark-api_3.3.0_2.13:VERSION
2.12 kotlin-spark-api_3.3.0_2.12:VERSION
3.2.1 2.13 kotlin-spark-api_3.2.1_2.13:VERSION
2.12 kotlin-spark-api_3.2.1_2.12:VERSION
3.2.0 2.13 kotlin-spark-api_3.2.0_2.13:VERSION
2.12 kotlin-spark-api_3.2.0_2.12:VERSION
3.1.3 2.12 kotlin-spark-api_3.1.3_2.12:VERSION
3.1.2 2.12 kotlin-spark-api_3.1.2_2.12:VERSION
3.1.1 2.12 kotlin-spark-api_3.1.1_2.12:VERSION
3.1.0 2.12 kotlin-spark-api_3.1.0_2.12:VERSION
3.0.3 2.12 kotlin-spark-api_3.0.3_2.12:VERSION
3.0.2 2.12 kotlin-spark-api_3.0.2_2.12:VERSION
3.0.1 2.12 kotlin-spark-api_3.0.1_2.12:VERSION
3.0.0 2.12 kotlin-spark-api_3.0.0_2.12:VERSION

Where VERSION, at the time of writing, is “1.2.1”.

Conclusion

These are the most important changes to the Kotlin API for Apache Spark version 1.2. If you want to take a deeper look into everything that’s changed, we now use Milestones to keep track of the changes, as well as Github Releases.

As usual, you can find the release on Maven Central.

In other news, we now also publish SNAPSHOT versions to GitHub Packages during development. If you want to be the first to try out new features and provide us with feedback, give it a try!

If you have any ideas or if you need help or support, please contact us on Slack or GitHub issues. We gladly welcome any feedback or feature requests!
As usual, thanks to Pasha Finkelshteyn for his continued support during the development of the project!

Continue ReadingKotlin API for Apache Spark v1.2: UDTs, UDFs, RDDs, Compatibility, and More!

Kotlin API for Apache Spark v1.2: UDTs, UDFs, RDDs, Compatibility, and More!

Hi everyone, Jolan here, with my first actual blog post! It’s been a couple of months since the last release of the Kotlin API for Apache Spark and we feel like all of the exciting changes since then are worth another announcement.

Let me remind you what the Kotlin API for Apache Spark is and why it was created. Apache Spark is a framework for distributed computations, which data engineers usually use to solve different tasks, like in the ETL process. It supports multiple languages straight out of the box, including Java, Scala, Python, and R. We at JetBrains are committed to supporting one more language for Apache Spark – Kotlin. We believe it can combine multiple pros from other language APIs while avoiding their cons.

If you don’t want to read about it and just want to try it out – here is the link to the repo.

Kotlin Spark API on GitHub

Otherwise, let’s begin!

UDTs & UDFs

User-defined types (or UDTs) are Spark’s go-to method for allowing any instance of a data-containing class to be stored in and read from a Dataset. One of the extensions for Spark that we now fully support which uses these types is Mllib, a library that aims to make practical machine learning scalable and easy. An example using Mllib can be found here (in Datalore). To define your own UDTs, here’s another example (in Datalore).

While it’s generally recommended to use Spark’s column functions for your Dataset- or SQL operations, sometimes a User-defined function (UDF) or User-defined aggregate function (UDAF) can provide extra flexibility. While the Kotlin Spark API did contain basic UDF support, we have greatly expanded that support. If you want, you can jump straight to all examples (in Datalore).

Similar to Spark’s API, you can create a UDF or UDAF and register it to make it executable from SQL. You can follow the original API, but you can also use some neat Kotlin tricks to make it a bit easier:

withSpark {
    val plusOne by udf { x: Int -> x + 1 }
    plusOne.register()

    spark.sql("SELECT plusOne(5)").showDS()
    //    +---------------+
    //    |     plusOne(5)|
    //    +---------------+
    //    |              6|
    //    +---------------+

}

In contrast to Spark’s API, the created UDFs are now also typed. This means that when you directly invoke a UDF instance using Dataset.select() calls with the correctly typed TypedColumns, the UDF will return the correct types too:

data class User(val name: String, val age: Int)

withSpark {
    val toString by udf { x: Int -> x.toString() }
    
    val ds = dsOf(User(...), ...) // Dataset<User>

    val newDs = ds.select(
       col(User::name), 
       toString(col(User::age)),
    ) // Dataset<Tuple2<String, String>> 
}

Finally, we added simple Vararg-UDFs, which do not even exist in the Scala API. This allows a UDF to accept any number of same-type arguments and works both from SQL and the Dataset invocations.

(Note, the following example also works when you define a UDF the “normal” way using an array of any type as a single argument.)

fun sumOf(vararg double: Double): Double = double.sum() 

withSpark {
    udf.register(::sumOf)
    
    spark.sql("SELECT sumOf(1.0, 2.0, 3.0), sumOf(5.0)").showDS()
    //    +--------------------+----------+
    //    |sumOf(1.0, 2.0, 3.0)|sumOf(5.0)|
    //    +--------------------+----------+
    //    |                 6.0|       5.0|
    //    +--------------------+----------+

}

We strongly encourage you to take a look at the examples (in Datalore) since they go over everything that’s possible with the new UDFs, and most importantly, we encourage you to play around with them and let us know what you think!

RDDs

Resilient Distributed Datasets (RDDs) are the backbone of Apache Spark. While in most cases typed Datasets are preferred, RDDs still have a special place thanks to their flexibility and speed. They are also still used under the hood of Datasets. While the Java API of RDDs is perfectly usable from Kotlin, we thought we could port over some of the niceties from the Scala API.

Similar to Scala, when you’re working with an RDD with the type Tuple2, you can utilize all kinds of handy key / value functions, like aggregateByKey(), reduceByKey(), groupByKey(), filterByRange(), and values() – no mapToPair necessary!

This also holds true for RRDs with a numeric type. Whereas previously you’d need to mapToDouble, you can now call mean(), sum(), stdev()max(), etc. directly.

Finally, we added some Kotlin-esque creation functions surrounding RDDs, such as rddOf()List.toRDD(), and RDD.toDS().
For an extensive example surrounding generating unique groups of indices using RDDs, look no further (in Datalore).

Jupyter

We are excited to see people prototyping and preprocessing their data using Spark from Jupyter notebooks in Kotlin! We further enhanced support for this by adding some essential features:

While adding %use spark is usually enough to get you going, sometimes it’s essential to change some of the properties Spark uses. This is why from release v1.2 (and the Kotlin Jupyter kernel v0.11.0-134 and up), you can now define Spark properties directly in the use-magic!

For example:  %use spark(spark=3.3.0, scala=2.12, spark.master=local[4]).

Display options were also overhauled; You can now set them using a nice little DSL:

sparkProperties {
    displayLimit = 2
    displayTruncate = -1
}

This specific example will limit the number of rows displayed to only 2 and remove the truncation of text in cells entirely. These properties can also be provided in the use-magic.
Check Datalore for an example!

Compatibility

The largest changes for this release happened under the hood. We migrated the build system from Maven to Gradle and designed a new building method so that we can (as of right now) build 14 different versions of the Kotlin Spark API at once!

“Why 14 versions?” you might ask. Well… it’s complicated… But let me attempt to explain. You can also skip this and just jump to the new naming scheme below.

While most projects using Spark operate just fine with a patch-version change, the Kotlin Spark API works by replacing a core file of Spark. This file only operates well with the patch version it was created for, thanks to Spark using reflection to call parts of it.

Before, we would push releases just for each latest minor version of Spark (e.g. 3.0.3, 3.1.3, and 3.2.1). This worked fine, as long as you used the same exact Spark version. However, not everyone has the luxury of having the latest patch version of Spark, or even the same Scala version, especially when bound to an external server environment.

To ensure all of our users are able to use the latest version of the Kotlin Spark API, no matter their setup, we now build our API for all patch versions and all core Scala versions of Apache Spark from 3.0.0+, as found on Maven Central, using a preprocessor and lots of tests to solve the compatibility issues found in between versions.

The new naming scheme for the packages will now contain both the Apache Spark patch version, as well as the Scala core version:

[name]_[Apache Spark version]_[Scala core version]:[Kotlin for Apache Spark API version]

The only exception to this is scala-tuples-in-kotlin_[Scala core version]:[Kotlin for Apache Spark API version], which is independent of Spark and can be used on its own without Spark in any of your Kotlin / Scala hybrid projects!

The new release scheme looks as follows:

Apache Spark Scala Kotlin Spark API
3.3.0 2.13 kotlin-spark-api_3.3.0_2.13:VERSION
2.12 kotlin-spark-api_3.3.0_2.12:VERSION
3.2.1 2.13 kotlin-spark-api_3.2.1_2.13:VERSION
2.12 kotlin-spark-api_3.2.1_2.12:VERSION
3.2.0 2.13 kotlin-spark-api_3.2.0_2.13:VERSION
2.12 kotlin-spark-api_3.2.0_2.12:VERSION
3.1.3 2.12 kotlin-spark-api_3.1.3_2.12:VERSION
3.1.2 2.12 kotlin-spark-api_3.1.2_2.12:VERSION
3.1.1 2.12 kotlin-spark-api_3.1.1_2.12:VERSION
3.1.0 2.12 kotlin-spark-api_3.1.0_2.12:VERSION
3.0.3 2.12 kotlin-spark-api_3.0.3_2.12:VERSION
3.0.2 2.12 kotlin-spark-api_3.0.2_2.12:VERSION
3.0.1 2.12 kotlin-spark-api_3.0.1_2.12:VERSION
3.0.0 2.12 kotlin-spark-api_3.0.0_2.12:VERSION

Where VERSION, at the time of writing, is “1.2.1”.

Conclusion

These are the most important changes to the Kotlin API for Apache Spark version 1.2. If you want to take a deeper look into everything that’s changed, we now use Milestones to keep track of the changes, as well as Github Releases.

As usual, you can find the release on Maven Central.

In other news, we now also publish SNAPSHOT versions to GitHub Packages during development. If you want to be the first to try out new features and provide us with feedback, give it a try!

If you have any ideas or if you need help or support, please contact us on Slack or GitHub issues. We gladly welcome any feedback or feature requests!
As usual, thanks to Pasha Finkelshteyn for his continued support during the development of the project!

Continue ReadingKotlin API for Apache Spark v1.2: UDTs, UDFs, RDDs, Compatibility, and More!

Kotlin API for Apache Spark 1.0 Released

The Kotlin API for Apache Spark is now widely available. This is the first stable release of the API that we consider to be feature-complete with respect to the user experience and compatibility with core Spark APIs.

Get on Maven Central

Let’s take a look at the new features this release brings to the API.

Typed select and sort

The Scala API has a typed select method that returns Datasets of Tuples. Sometimes using them can be more idiomatic or convenient than using the map function. Here’s what the syntax for this method looks like:

case class TestData(id: Long, name: String, url: String)
// ds is of type Dataset[TestData]
val result: Dataset[Tuple2[String, Long]] = 
        ds.select($"name".as[String], $"id".as[Long])

Sometimes obtaining just a tuple may be really convenient, but this method has a drawback: you have to select a column by name and explicitly provide the type. This can lead to errors, which might be hard to fix in long pipelines.

We’re trying to address this issue at least partially in our extension to the Scala API. Consider the following Kotlin code:

data class TestData(val id: Long, val name: String, val url: String)
// ds is of type Dataset<TestData>
val result: Dataset<Arity2<String, Long>> = 
        ds.selectTyped(TestData::name, TestData::id)

The result is the same, but the call is entirely type-safe. We do not use any strings and casts, and both the column name and the type are inferred from reflection.

We have also added a similarly reflective syntax to the sort function.

In Scala, this API supports arities up to 5, and we decided to be as consistent with the Scala API as possible. We also think that the usage of tuples with arities above five is an indication that something is going wrong. For example, maybe it would be better to extract a new domain object or, conversely, to work with untyped datasets.

More column functions

The Scala API is very rich in terms of functions that can be called on columns. We cannot make them identical to the Scala API because of the limitations of Kotlin. For example, overriding class members with extensions is forbidden, and the Dataset class is not extensible. But we can at least use infix functions and names in backticks to implement operator-like functions.

Here are the operator-like functions that we currently support:

  • ==
  • !=
  • eq / `===`
  • neq / `=!=`
  • -col(...)
  • !col(...)
  • gt
  • lt
  • geq
  • leq
  • or
  • and / `&&`
  • +
  • -
  • *
  • /
  • %

Luckily, we can see that very few of the functions require backticks, and those that do can be autocompleted without you having to type them.

More KeyValueGroupedDataset wrapper functions

We initially designed the API so that anyone could call any function that requires a Decoder and Encoder simply by using the magic <em>encoder()</em> function, which generates everything automagically. It gave our users some flexibility, and it also allowed us not to implement all the functions that the Dataset API offers. But we would ultimately like to provide our users with the best developer experience possible. This is why we’ve implemented necessary wrappers over KeyValueGroupedDataset, and also why we’ve added support for the following functions:

  • cogroup
  • flatMapGroupsWithState
  • mapGroupsWithState

Support for Scala TupleN classes

There are several APIs in the Spark API that return Datasets of Tuples. Examples of such APIs are the select and joinWith functions. Before this release, users had to manually find an encoder for tuples:

val encoder = Encoders.tuple(Encoders.STRING(), Encoders.INT())
ds
    .select(ds.col("a").`as`<String>, ds.col("b").`as`<Int>)
    .map({ Tuple2(it._1(), it._2() + 1) }, encoder)

And the more we work with tuples, the more encoders we need, which leads to verbosity and requires us to find increasing numbers of names for new encoders.

After this change, code becomes as simple as any usual Kotlin API code:

ds
    .select(ds.col("a").`as`<String>, ds.col("b").`as`<Int>)
    .map { Tuple2(it._1(), it._2() + 1) }

You no longer need to rely on specific encoders or lambdas inside argument lists.

Support for date and time types

Work with dates and times is an important part of many data engineering workflows. For Spark 3.0, we had default encoders registered for Date and Timestamp, but inside data structures we had support only for LocalDate and Instant, which is obviously not enough. We now have full support for LocalDate, Date, Timestamp, and Instant both as top-level entities of dataframes and as fields inside of structures.

We have also added support for Date and Timestamp as fields inside of structures for Spark 2.

Support for maps encoded as tuples

There is a well-known practice of encoding maps as tuples. For example, rather than storing the ID of an entity and the name of the entity in the map, it is fairly common to store them in two columns in a structure like Dataset<Pair<Long, String>> (which is how relational databases usually work).

We are aware of this, and we’ve decided to add support for working with such datasets in the same way you work with maps. We have added the functions takeKeys and takeValues to Dataset<Tuple2<T1, T2>>, Dataset<Pair<T1, T2>>, and Dataset<Arity2<T1, T2>>.

Conclusion

We want to say a huge “thank you” to Jolan Rensen, who helped us tremendously by offering feedback, assisting with the implementation of features, and fixing bugs in this release. He worked with our project while writing his thesis, and we’re happy that we can help him with his brilliant work. If you want to read more about Jolan, please visit his site.

If you want to read more about the details of the new release, please check out the changelog.

As usual, the latest release is available at Maven Central. And we would love to get your feedback, which you can leave in:

Continue ReadingKotlin API for Apache Spark 1.0 Released

Kotlin for Apache Spark: One Step Closer to Your Production Cluster

We have released Preview 2 of Kotlin for Apache Spark. First of all, we’d like to thank the community for providing us with all their feedback and even some pull requests! Now let’s take a look at what we have implemented since Preview 1?

Scala 2.11 and Spark 2.4.1+ support

The main change in this preview is the introduction of Spark 2.4 and Scala 2.11 support. This means that you can now run jobs written in Kotlin in your production environment.

The syntax remains the same as the Apache Spark 3.0 compatible version, but the installation procedure differs a bit. You can now use sbt to add the correct dependency, for example:

libraryDependencies += "org.jetbrains.kotlinx.spark" %% "kotlin-spark-api-2.4" % "1.0.0-preview2"

Of course, for other build systems, such as Maven, we would use the standard:

<dependency>
  <groupId>org.jetbrains.kotlinx.spark</groupId>
  <artifactId>kotlin-spark-api-2.4_2.11</artifactId>
  <version>1.0.0-preview2</version>
</dependency>

And for Gradle we would use:

implementation 'org.jetbrains.kotlinx.spark:kotlin-spark-api-2.4_2.11:1.0.0-preview2'

Spark 3 only supports Scala 2.12 for now, so there is no need to define it.

You can read more about dependencies here.

Support for the custom SparkSessionBuilder

Imagine you have a company-wide SparkSession.Builder that is set up to work with your YARN/Mesos/etc. cluster. You don’t want to create it, and the “withSpark” function doesn’t give you enough flexibility. Before Preview 2, you had only one option – you had to rewrite the whole builder from Scala to Kotlin. You now have another one, as the “withSpark” function accepts the SparkSession.Builder as an argument:

val builder = // obtain your builder here
withSpark(builder, logLevel = DEBUG) {
  // your spark is initialized with correct config here
}

Broadcast variable support

Sometimes we need to share data between executor nodes. Occasionally, we need to provide executor nodes with dictionary-like data without pushing it to each executor node individually. Apache Spark allows you to do this using broadcasting variable. You were previously able to do this using the “encoder” function. Still, we are aiming to provide an API experience that is as consistent as possible with that of the Scala API. So we’ve added explicit support for the broadcasting of variables. For example:

// Broadclasted data should implement Serializable
data class SomeClass(val a: IntArray, val b: Int) : Serializable
// some another code snipped
val receivedBroadcast = broadcast.value // broadcasted value is available

We’d like to thank Jolan Rensen for the contribution.

Primitive arrays support for Spark 3

If you use arrays of primitives in your data pipeline, you may be glad to know that they now work in Kotlin for Apache Spark. Arrays of data (or Java bean) classes were supported in Preview 1, but we’ve only just added support for primitive arrays in Preview 2. Please note that this support will work correctly only if you’re using particular Kotlin primitive array types, such as IntArray, BooleanArray, and LongArray. In other cases, we propose using lists or alternative collection types.

Future work

Now that Spark 2.4 is supported, our targets for Preview 3 will be mainly to extend the Column API and add more operations on columns. Once Apache Spark with Scala 2.13 support is available, we will implement support for Scala 2.13, as well.

The awesome support introduced in Preview 2 makes now a great time to give it a try! The official repository and the quick start guide are excellent places to start.

Continue ReadingKotlin for Apache Spark: One Step Closer to Your Production Cluster

End of content

No more pages to load