Skip to content

Commit

Permalink
[DO NOT MERGE] prototype to use a rust experiments api and expose it …
Browse files Browse the repository at this point in the history
…through glean
  • Loading branch information
Tarik Eshaq committed Jul 22, 2020
1 parent 200f9d3 commit c243d5e
Show file tree
Hide file tree
Showing 12 changed files with 661 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .buildconfig.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
libraryVersion: 31.4.0
libraryVersion: 31.4.0-TESTING28
groupId: org.mozilla.telemetry
projects:
glean:
Expand Down
200 changes: 195 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ buildscript {
// Docs generation
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:$versions.dokka"

classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
Expand Down
32 changes: 32 additions & 0 deletions glean-core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.dokka-android'
apply plugin: 'jacoco'
apply plugin: 'com.google.protobuf'

/*
* This defines the location of the JSON schema used to validate the pings
Expand Down Expand Up @@ -65,6 +66,13 @@ android {
}

sourceSets {
// We define where the gradle tasks can find
// the .proto file used in the generation
main {
proto {
srcDir '../src'
}
}
main.jniLibs.srcDirs += "$buildDir/nativeLibs/android"
test.resources.srcDirs += "$buildDir/rustJniLibs/desktop"
test.resources.srcDirs += "$buildDir/nativeLibs/desktop"
Expand Down Expand Up @@ -172,13 +180,37 @@ configurations {
jnaForTest
}

// We generate a bunch of gradle tasks that will help us
// generate the kotlin files associated with the .proto file
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.11.4'
}
plugins {
javalite {
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}

dependencies {
api "org.mozilla.components:concept-fetch:$versions.android_components"
jnaForTest "net.java.dev.jna:jna:$versions.jna@jar"
implementation "net.java.dev.jna:jna:$versions.jna@aar"

// Note: the following version must be kept in sync
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"
implementation 'com.google.protobuf:protobuf-javalite:3.11.4'

implementation "androidx.annotation:annotation:$versions.androidx_annotation"
implementation "androidx.lifecycle:lifecycle-extensions:$versions.androidx_lifecycle_extensions"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package mozilla.telemetry.glean

import android.content.Context
import android.os.Build
import com.sun.jna.Pointer
import mozilla.telemetry.glean.rust.LibGleanFFI
import mozilla.telemetry.glean.rust.RustError
import java.util.*
import java.util.concurrent.atomic.AtomicLong
import com.google.protobuf.CodedOutputStream
import com.google.protobuf.MessageLite
import com.sun.jna.Native
import java.nio.ByteBuffer
import java.nio.ByteOrder

// A LOT OF THIS IS COPIED FOR THE SAKE OF THE PROTOTYPE, NOT COMPLETE
fun <T : MessageLite> T.toNioDirectBuffer(): Pair<ByteBuffer, Int> {
val len = this.serializedSize
val nioBuf = ByteBuffer.allocateDirect(len)
nioBuf.order(ByteOrder.nativeOrder())
val output = CodedOutputStream.newInstance(nioBuf)
this.writeTo(output)
output.checkNoSpaceLeft()
return Pair(first = nioBuf, second = len)
}

open class ExperimentsInternalAPI internal constructor () {
private var raw: AtomicLong = AtomicLong(0)

fun initialize(
applicationContext: Context,
dbPath: String
) {
val appCtx = MsgTypes.AppContext.newBuilder()
.setAppId(applicationContext.packageName)
.setAppVersion(applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0).versionName)
.setDeviceManufacturer(Build.MANUFACTURER)
.setLocaleCountry(
try {
Locale.getDefault().isO3Country
} catch (e: MissingResourceException) {
Locale.getDefault().country
}
)
.setLocaleLanguage(
try {
Locale.getDefault().isO3Language
} catch (e: MissingResourceException) {
Locale.getDefault().language
}
)
.setDeviceModel(Build.DEVICE)
.build()
val (nioBuf, len) = appCtx.toNioDirectBuffer()
raw.set( rustCall { error ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibGleanFFI.INSTANCE.experiments_new(ptr, len, dbPath, error)
})
}

fun getBranch(experimentName: String): String {
var ptr = rustCall { error ->
LibGleanFFI.INSTANCE.experiments_get_branch(raw.get(), experimentName, error)
}
return ptr.getAndConsumeRustString()
}

/**
* Helper to read a null terminated String out of the Pointer and free it.
*
* Important: Do not use this pointer after this! For anything!
*/
internal fun Pointer.getAndConsumeRustString(): String {
return this.getRustString()
// PLEASE INSERT A FREE HERE!!!!!!!
}

/**
* Helper to read a null terminated string out of the pointer.
*
* Important: doesn't free the pointer, use [getAndConsumeRustString] for that!
*/
internal fun Pointer.getRustString(): String {
return this.getString(0, "utf8")
}

// In practice we usually need to be synchronized to call this safely, so it doesn't
// synchronize itself
private inline fun <U> nullableRustCall(callback: (RustError.ByReference) -> U?): U? {
val e = RustError.ByReference()
try {
val ret = callback(e)
if (e.isFailure()) {
// We ignore it for now, although we shouldn't just cuz protoype
//throw e.intoException()
}
return ret
} finally {
// This only matters if `callback` throws (or does a non-local return, which
// we currently don't do)
e.ensureConsumed()
}
}

private inline fun <U> rustCall(callback: (RustError.ByReference) -> U?): U {
return nullableRustCall(callback)!!
}
}

/**
* The main experiments object
* ```
*/
object Experiments : ExperimentsInternalAPI()
150 changes: 150 additions & 0 deletions glean-core/android/src/main/java/mozilla/telemetry/glean/HttpConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package mozilla.telemetry.glean

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// THIS IS A CLONE OF THE OTHER HTTPCONFIG USED FOR APPLICATION SERVICES
// SEEMED CONVIENIENT FOR THE SAKE OF THE PROTOTYPE, PREFERRABLY
// WE CAN USE THE SAME HTTPCONFIG, BUT I COULDN'T WRAP MY HEAD ON A QUICK
// WAY TO DO THAT FOR A PROTOTYPE

import com.google.protobuf.ByteString
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.telemetry.glean.rust.LibGleanFFI
import mozilla.telemetry.glean.rust.RawFetchCallback
import mozilla.telemetry.glean.rust.RustBuffer
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write

/**
* Singleton allowing management of the HTTP backend
* used by Rust components.
*/
object RustHttpConfig {
// Protects imp/client
private var lock = ReentrantReadWriteLock()
@Volatile
private var client: Lazy<Client>? = null
// Important note to future maintainers: if you mess around with
// this code, you have to make sure `imp` can't get GCed. Extremely
// bad things will happen if it does!
@Volatile
private var imp: CallbackImpl? = null

/**
* Set the HTTP client to be used by all Rust code.
* the `Lazy`'s value is not read until the first request is made.
*/
@Synchronized
fun setClient(c: Lazy<Client>) {
lock.write {
client = c
if (imp == null) {
imp = CallbackImpl()
LibGleanFFI.INSTANCE.viaduct_initialize(imp!!)
}
}
}

internal fun convertRequest(request: MsgTypes.Request): Request {
val headers = MutableHeaders()
for (h in request.headersMap) {
headers.append(h.key, h.value)
}
return Request(
url = request.url,
method = convertMethod(request.method),
headers = headers,
connectTimeout = Pair(request.connectTimeoutSecs.toLong(), TimeUnit.SECONDS),
readTimeout = Pair(request.readTimeoutSecs.toLong(), TimeUnit.SECONDS),
body = if (request.hasBody()) {
Request.Body(request.body.newInput())
} else {
null
},
redirect = if (request.followRedirects) {
Request.Redirect.FOLLOW
} else {
Request.Redirect.MANUAL
},
cookiePolicy = Request.CookiePolicy.OMIT,
useCaches = request.useCaches
)
}

@Suppress("TooGenericExceptionCaught", "ReturnCount")
internal fun doFetch(b: RustBuffer.ByValue): RustBuffer.ByValue {
lock.read {
try {
val request = MsgTypes.Request.parseFrom(b.asCodedInputStream())
val rb = try {
// Note: `client!!` is fine here, since if client is null,
// we wouldn't have yet initialized
val resp = client!!.value.fetch(convertRequest(request))
val rb = MsgTypes.Response.newBuilder()
.setUrl(resp.url)
.setStatus(resp.status)
.setBody(resp.body.useStream {
ByteString.readFrom(it)
})

for (h in resp.headers) {
rb.putHeaders(h.name, h.value)
}
rb
} catch (e: Throwable) {
MsgTypes.Response.newBuilder().setExceptionMessage("fetch error: ${e.message ?: e.javaClass.canonicalName}")
}
val built = rb.build()
val needed = built.serializedSize
val outputBuf = LibGleanFFI.INSTANCE.viaduct_alloc_bytebuffer(needed)
try {
// This is only null if we passed a negative number or something to
// viaduct_alloc_bytebuffer.
val stream = outputBuf.asCodedOutputStream()!!
built.writeTo(stream)
return outputBuf
} catch (e: Throwable) {
// Note: we want to clean this up only if we are not returning it to rust.
LibGleanFFI.INSTANCE.viaduct_destroy_bytebuffer(outputBuf)
LibGleanFFI.INSTANCE.viaduct_log_error("Failed to write buffer: ${e.message}")
throw e
}
} finally {
LibGleanFFI.INSTANCE.viaduct_destroy_bytebuffer(b)
}
}
}
}

internal fun convertMethod(m: MsgTypes.Request.Method): Request.Method {
return when (m) {
MsgTypes.Request.Method.GET -> Request.Method.GET
MsgTypes.Request.Method.POST -> Request.Method.POST
MsgTypes.Request.Method.HEAD -> Request.Method.HEAD
MsgTypes.Request.Method.OPTIONS -> Request.Method.OPTIONS
MsgTypes.Request.Method.DELETE -> Request.Method.DELETE
MsgTypes.Request.Method.PUT -> Request.Method.PUT
MsgTypes.Request.Method.TRACE -> Request.Method.TRACE
MsgTypes.Request.Method.CONNECT -> Request.Method.CONNECT
}
}

internal class CallbackImpl : RawFetchCallback {
@Suppress("TooGenericExceptionCaught")
override fun invoke(b: RustBuffer.ByValue): RustBuffer.ByValue {
try {
return RustHttpConfig.doFetch(b)
} catch (e: Throwable) {
LibGleanFFI.INSTANCE.viaduct_log_error("doFetch failed: ${e.message}")
// This is our last resort. It's bad news should we fail to
// return something from this function.
return RustBuffer.ByValue()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@

package mozilla.telemetry.glean.rust

import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.StringArray
import com.sun.jna.*
import java.lang.reflect.Proxy
import mozilla.telemetry.glean.config.FfiConfiguration
import mozilla.telemetry.glean.net.FfiPingUploadTask
Expand Down Expand Up @@ -567,4 +564,21 @@ internal interface LibGleanFFI : Library {
// Misc

fun glean_str_free(ptr: Pointer)

// DO NOT MERGE ME PLEASE. THIS WAS ONLY ADDED FOR PROTOTYPING PURPOSES!!!
fun experiments_new(appContext: Pointer, appContextLen: Int, dbPath: String, error: RustError.ByReference): Long

fun experiments_get_branch(handle: Long, branchName: String, error: RustError.ByReference): Pointer?

fun viaduct_destroy_bytebuffer(b: RustBuffer.ByValue)
// Returns null buffer to indicate failure
fun viaduct_alloc_bytebuffer(sz: Int): RustBuffer.ByValue
// Returns 0 to indicate redundant init.
fun viaduct_initialize(cb: RawFetchCallback): Byte

fun viaduct_log_error(s: String)
}

internal interface RawFetchCallback: Callback {
fun invoke(b: RustBuffer.ByValue): RustBuffer.ByValue
}
Loading

0 comments on commit c243d5e

Please sign in to comment.