Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Server support #447

Merged
merged 18 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Thrifty
[![codecov](https://codecov.io/gh/Microsoft/thrifty/branch/master/graph/badge.svg)](https://codecov.io/gh/Microsoft/thrifty)


Thrifty is an implementation of the Apache Thrift software stack for Android, which uses 1/4 of the method count taken by
the Apache Thrift compiler.
Thrifty is an implementation of the Apache Thrift software stack, which uses 1/4 of the method count taken by
the Apache Thrift compiler, which makes it especially appealing for use on Android.

Thrift is a widely-used cross-language service-definition software stack, with a nifty interface definition language
from which to generate types and RPC implementations. Unfortunately for Android devs, the canonical implementation
generates very verbose and method-heavy Java code, in a manner that is not very Proguard-friendly.

Like Square's Wire project for Protocol Buffers, Thrifty does away with getters and setters (and is-setters and
set-is-setters) in favor of public final fields. It maintains some core abstractions like Transport and Protocol, but
saves on methods by dispensing with Factories and server implementations and only generating code for the
saves on methods by dispensing with Factories, omitting server implementations by default and only generating code for the
protocols you actually need.

Thrifty was born in the Outlook for Android codebase; before Thrifty, generated thrift classes consumed 20,000 methods.
Expand Down Expand Up @@ -94,7 +94,7 @@ The major differences are:
- Thrifty structs are always valid, once built.
- Fields that are neither required nor optional (i.e., "default") are treated as optional; a struct with an unset default field may still be serialized.
- TupleProtocol is unsupported at present.
- Server-specific features from Apache's implementation are not duplicated in Thrifty.
- Server-specific features are only supported for Kotlin code generation and currently considered experimental

## Guide To Thrifty

Expand Down Expand Up @@ -323,6 +323,7 @@ java -jar thrifty-compiler.jar \
--kt-file-per-type \
--omit-file-comments \
--kt-struct-builders \
--experimental-kt-generate-server \
...
```

Expand All @@ -346,6 +347,22 @@ Builders are unnecessary, and are not included by default. For compatibility wi

By default, Thrifty generates one Kotlin file per JVM package. For larger thrift files, this can be a little hard on the Kotlin compiler. If you find build times or IDE performance suffering, the `--kt-file-per-type` flag can help. Outlook Mobile's single, large, Kotlin file took up to one minute just to typecheck, using Kotlin 1.2.51! For these cases, `--kt-file-per-type` will tell Thrifty to generate one single file per top-level class - just like the Java code.

`--experimental-kt-generate-server` enabled code generation for the server portion of a thrift service. You can
use this to implement a thrift server with the same benefits as the kotlin client: no runtime surprises thanks to
structs being always valid by having nullability guarantees and unions represented as sealed classes.
See [Server Support](#server-support).

#### Server Support
Support for generating a Kotlin server implementation was only added very recently, and while it passes the 'official'
[Java client integration test](https://github.com/apache/thrift/blob/master/lib/java/test/org/apache/thrift/test/TestClient.java),
you should consider this code experimental.

Thrifty generates a `Processor` implementation that you pass an input `Protocol`, an output `Protocol` and a service handler
and the code will take care of reading the request, passing it to the handler and returning the correct response to the output.

If you want to use it, you need to wrap an appropriate communication layer around it, e.g. an HTTP server.
You can have a look at the [integration tests](thrifty-integration-tests/src/test/kotlin/com/microsoft/thrifty/integration/conformance/server/TestServer.kt) for a basic example.

### Java-specific command-line options

Thrifty can be made to add various kinds of nullability annotations to Java types with the `--nullability-annotation-type` flag. Valid options are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ class ThriftyCompiler {
help = "When set, don't generate service clients")
.flag(default = false)

val generateServer: Boolean by option("--experimental-kt-generate-server",
help = "When set, generate kotlin server implementation (EXPERIMENTAL)")
.flag(default = false)

val omitFileComments: Boolean by option("--omit-file-comments",
help = "When set, don't add file comments to generated files")
.flag(default = false)
Expand Down Expand Up @@ -349,6 +353,10 @@ class ThriftyCompiler {
gen.omitServiceClients()
}

if (generateServer) {
gen.generateServer()
}

if (kotlinEmitJvmName) {
gen.emitJvmName()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ private void generateKotlinThrifts(Schema schema, SerializableThriftOptions opts
gen.omitServiceClients();
}

if (kopt.isGenerateServer()) {
gen.generateServer();
}

if (opts.getListType() != null) {
gen.listClassName(opts.getListType());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public enum ClientStyle {

private ClientStyle serviceClientStyle = null;
private boolean structBuilders = false;
private boolean generateServer = false;

@Input
@Optional
Expand Down Expand Up @@ -91,4 +92,13 @@ public boolean getStructBuilders() {
public void setStructBuilders(boolean structBuilders) {
this.structBuilders = structBuilders;
}

@Input
public boolean isGenerateServer() {
return generateServer;
}

public void setGenerateServer(boolean generateServer) {
this.generateServer = generateServer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ class SerializableThriftOptions implements Serializable {
static class Kotlin implements Serializable {
private ClientStyle serviceClientStyle;
private boolean structBuilders;
private boolean generateServer;

// Required for Serializable
Kotlin() {}

public Kotlin(ClientStyle serviceClientStyle, boolean structBuilders) {
public Kotlin(ClientStyle serviceClientStyle, boolean structBuilders, boolean generateServer) {
this.serviceClientStyle = serviceClientStyle;
this.structBuilders = structBuilders;
this.generateServer = generateServer;
}

public ClientStyle getServiceClientStyle() {
Expand All @@ -47,6 +49,10 @@ public ClientStyle getServiceClientStyle() {
public boolean isStructBuilders() {
return structBuilders;
}

public boolean isGenerateServer() {
return generateServer;
}
}

static class Java implements Serializable {
Expand Down Expand Up @@ -88,7 +94,7 @@ public NullabilityAnnotations getNullabilityAnnotations() {

if (options instanceof KotlinThriftOptions) {
KotlinThriftOptions kto = (KotlinThriftOptions) options;
this.kotlinOpts = new Kotlin(kto.getServiceClientStyle(), kto.getStructBuilders());
this.kotlinOpts = new Kotlin(kto.getServiceClientStyle(), kto.getStructBuilders(), kto.isGenerateServer());
} else if (options instanceof JavaThriftOptions) {
JavaThriftOptions jto = (JavaThriftOptions) options;
this.javaOpts = new Java(jto.getNullabilityAnnotations());
Expand Down
3 changes: 3 additions & 0 deletions thrifty-integration-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ dependencies {

testImplementation deps.bundles.kotlin
testImplementation deps.bundles.testing

implementation 'org.apache.thrift:libthrift:0.12.0'
}

sourceSets {
Expand Down Expand Up @@ -75,6 +77,7 @@ def kompileTestThrift = tasks.register("kompileTestThrift", JavaExec) { t ->
"--map-type=java.util.LinkedHashMap",
"--set-type=java.util.LinkedHashSet",
"--list-type=java.util.ArrayList",
"--experimental-kt-generate-server",
"$projectDir/ClientThriftTest.thrift"
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Thrifty
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
* WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
* FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
*
* See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
*/
package com.microsoft.thrifty.integration.conformance.server

import com.microsoft.thrifty.testing.ServerProtocol
import com.microsoft.thrifty.testing.TestClient
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import java.io.Closeable
import java.security.Permission

class BinaryServerConformanceTest : KotlinServerConformanceTest(ServerProtocol.BINARY)
class CompactServerConformanceTest : KotlinServerConformanceTest(ServerProtocol.COMPACT)
class JsonServerConformanceTest : KotlinServerConformanceTest(ServerProtocol.JSON)
/**
* A test of auto-generated service code for the standard ThriftTest
* service.
*
* Conformance is checked by roundtripping requests from a java client generated
* by the official Thrift generator to the server implementation generated by Thrifty.
* The test server has an implementation of ThriftTest methods with semantics as described in the
* .thrift file itself and in the Apache Thrift git repo
*/
abstract class KotlinServerConformanceTest(
private val serverProtocol: ServerProtocol
) {
protected class ExitException(val status: Int) : Exception()

private class NoExitSecurityManager : SecurityManager() {
override fun checkPermission(perm: Permission) {
// allow anything.
}

override fun checkPermission(perm: Permission, context: Any) {
// allow anything.
}

override fun checkExit(status: Int) {
throw ExitException(status)
}
}

class NoExit : Closeable {
init {
System.setSecurityManager(NoExitSecurityManager())
}

override fun close() {
System.setSecurityManager(null)
}
}

@JvmField
@RegisterExtension
val testServer = TestServer(serverProtocol)

@Test
fun testServer() {
val port = testServer.port()
val protocol = when (serverProtocol) {
ServerProtocol.BINARY -> "binary"
ServerProtocol.COMPACT -> "compact"
ServerProtocol.JSON -> "json"
}
val res = shouldThrow<ExitException> {
NoExit().use {
TestClient.main(arrayOf(
"--host=localhost",
"--port=$port",
"--transport=http",
"--protocol=$protocol"
))
}
}
res.status shouldBe 0
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Thrifty
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
* WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
* FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
*
* See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
*/
package com.microsoft.thrifty.integration.conformance.server

import com.microsoft.thrifty.integration.kgen.ThriftTestProcessor
import com.microsoft.thrifty.protocol.BinaryProtocol
import com.microsoft.thrifty.protocol.CompactProtocol
import com.microsoft.thrifty.protocol.JsonProtocol
import com.microsoft.thrifty.protocol.Protocol
import com.microsoft.thrifty.testing.ServerProtocol
import com.microsoft.thrifty.transport.Transport
import com.sun.net.httpserver.HttpContext
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import kotlinx.coroutines.runBlocking
import okio.Buffer
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.Extension
import org.junit.jupiter.api.extension.ExtensionContext
import java.net.InetSocketAddress
import java.util.concurrent.Executors


class TestServer(private val protocol: ServerProtocol = ServerProtocol.BINARY) : Extension, BeforeEachCallback,
AfterEachCallback {
val processor = ThriftTestProcessor(ThriftTestHandler())
private var server: HttpServer? = null

class TestTransport(
val b: Buffer = Buffer()
) : Transport {

override fun read(buffer: ByteArray, offset: Int, count: Int) = b.read(buffer, offset, count)

override fun write(buffer: ByteArray, offset: Int, count: Int) {
b.write(buffer, offset, count)
}

override fun flush() = b.flush()

override fun close() = b.close()
}

private fun handleRequest(exchange: HttpExchange) {
val inputTransport = TestTransport(Buffer().readFrom(exchange.requestBody))
val outputTransport = TestTransport()

val input = protocolFactory(inputTransport)
val output = protocolFactory(outputTransport)

runBlocking {
processor.process(input, output)
}

exchange.sendResponseHeaders(200, outputTransport.b.size)
exchange.responseBody.use {
outputTransport.b.writeTo(it)
}
}

fun run() {
server = HttpServer.create(InetSocketAddress("localhost", 0), 0).apply {
val context: HttpContext = createContext("/")
context.setHandler(::handleRequest)

executor = Executors.newSingleThreadExecutor()
start()
}
}

fun port(): Int {
return server!!.address.port
}

override fun beforeEach(context: ExtensionContext) {
run()
}

override fun afterEach(context: ExtensionContext) {
cleanupServer()
}

fun close() {
cleanupServer()
}

private fun cleanupServer() {
server?.let {
it.stop(0)
server = null
}
}

private fun protocolFactory(transport: Transport): Protocol = when (protocol) {
ServerProtocol.BINARY -> BinaryProtocol(transport)
ServerProtocol.COMPACT -> CompactProtocol(transport)
ServerProtocol.JSON -> JsonProtocol(transport)
else -> throw AssertionError("Invalid protocol value: $protocol")
}

}
Loading