marcoferrer / kroto-plus

gRPC Kotlin Coroutines, Protobuf DSL, Scripting for Protoc
Apache License 2.0
493 stars 28 forks source link

Adding Kroto Plus to a project with Springfox Swagger2 causes infinite loop before start #96

Open mattdkerr opened 4 years ago

mattdkerr commented 4 years ago

Arrangement: Project with a Spring Web MVC @RestController that uses a protobuf generated class as an input, and calls the same server code as the @GrpcSpringService (using https://github.com/yidongnan/grpc-spring-boot-starter). Kroto Plus configured to generate builders, message extensions, and coroutines for the service and messages defined in the proto files.

Kroto Plus version 0.6.0-SNAPSHOT was used

/*
 * Copyright (C) 2019 Electronic Arts Inc. All rights reserved.
 */

package com.ea.poutine.roolz.rlzservice

import com.ea.p.r.r.api.RController
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import springfox.documentation.builders.PathSelectors
import springfox.documentation.builders.RequestHandlerSelectors
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spring.web.plugins.Docket
import springfox.documentation.swagger2.annotations.EnableSwagger2

@SpringBootApplication
@EnableSwagger2
// import the specific ones we want Swagger exposed for, rather than let it wander around scanning controllers in the packages
@ComponentScan(basePackageClasses = [RController::class])
class Swagger2SpringBoot {
    companion object {
        lateinit var appContext: ConfigurableApplicationContext

        @JvmStatic
        fun main(args: Array<String>) {
            appContext = SpringApplication.run(Swagger2SpringBoot::class.java, *args)
        }
    }

    @Bean
    fun api(): Docket {
        return Docket(DocumentationType.SWAGGER_2)
            .select()
            .apis(RequestHandlerSelectors.any())
            .paths(PathSelectors.any())
            .build()
    }
}
@SpringBootApplication
@ComponentScan("com.ea.p")
@EnableDiscoveryClient
@EnableConfigurationProperties
class RApp {
    companion object {
        private val logger = KotlinLogging.logger { }

        @JvmStatic
        fun main(args: Array<String>) {
            logger.debug { "Running RApplication with args: ${Klaxon().toJsonString(args)}" }
            SpringApplication.run(RApp::class.java, *args)
        }

        @Bean
        fun init() = CommandLineRunner {
            logger.debug { "init called" }
        }

        @Bean
        fun userProvider(): UserProvider {
            return ThreadLocalUserProvider()
        }
    }
}
@RestController
@RequestMapping("/v3/r")
class RController @Autowired constructor(
    private val service: RService,
    fConverter: FConverter,
    private val timers: Timers,
    private val rResultConverter: rResultConverter,
    private val container: KContainer
) {
    private val bEvaluator = BEvaluator(service, fConverter)
    private val logger = KotlinLogging.logger { }

    @PostMapping(value = ["/tenant/{tenantId}/entity/{entityId}"],
        consumes = ["application/x-protobuf", "application/json"],
        produces = ["application/x-protobuf", "application/json"])
    fun ruleResultRequest(
        @PathVariable("tenantId") tenantId: String,
        @PathVariable("entityId") entityId: String,
        @RequestBody eRequests: BERequest
    ): ResponseEntity<BRResults> = runBlocking {
        val tags = mapOf("actorKey" to PAKey(entityId, tenantId).toString())
        withLoggingContext(tags) {
            respond(
                // rainbow brackets plugin for IntelliJ is useful here
                logicFunc = {
                    ruleResultConverter.toDto(
                        timers.recordAsync(MetricTypes.RRequestGrpc.id, tags) {
                            async {
                                bEvaluator.execute(evalRequests, entityId, tenantId)
                            }
                        }
                    )
                },
                afterFunc = { logger.info { "Finished all requests in list for $tenantId $entityId" } }
        ) }
    }

    @DeleteMapping("/tenant/{tenantId}/entity/{entityId}")
    fun deleteSession(
        @PathVariable("tenantId") tenantId: String,
        @PathVariable("entityId") entityId: String
    ): ResponseEntity<Boolean> {
        // this block wraps with try/catch with boilerplate logging, and is from the api-common library
        // returns an ResponseEntity.Ok(this return value as body) when it exits the respond block
        return respond(
            logicFunc = { service.deleteSession(PAKey(entityId, tenantId)) },
            afterFunc = { logger.info("Completed REST API to gRPC call for deleteSession for $tenantId $entityId") })
    }

    @GetMapping(value = ["/r"],
            produces = ["application/json"])
    fun getAllRNames(): ResponseEntity<List<String>> = runBlocking {
        val rNames = container.getRNames()
        val tags = mapOf("rNames" to rNames.toString())
        withLoggingContext(tags) {
            respond {
                logger.debug { "Finished returning all the rnames" }
                rNames
            }
        }
    }
}
mockServices:
  - filter:
      includePath:
        - com/ea/p/*
    implementAsObject: true
    generateServiceList: true
    serviceListPackage: com.ea.p.r
    serviceListName: MockRServices

protoBuilders:
  - filter:
      excludePath:
        - google/*
      includePath:
        - com/ea/p/*
    unwrapBuilders: true
    useDslMarkers: true

grpcCoroutines: []

grpcStubExts:
  - supportCoroutines: true

extendableMessages:
  - filter:
      includePath:
        - com/ea/p/r/*
syntax = "proto3";
import "google/protobuf/any.proto";
package com.ea.p.r;
option java_package = "com.ea.p.r.rp";
option java_multiple_files = true;
service RService {
    rpc bREvaluation (BERequest) returns (BRResults);
    rpc deleteSession (DeleteSessionRequest) returns (DeleteSessionResponse);
}

message BERequest {
    string tenantId = 1;
    string entityId = 2;
    repeated ERequestV3 eRequests = 3;
    Service service = 4; 
}

message BRResults {
    repeated EResponseV3 responses = 1;
}

message EResponseV3 {
    string associatedRequestId = 1;
    repeated RResultV3 rResults = 3;
}

message RResultV3 {
    string rId =1;
    bool state = 2;
    map<string, string> results = 3;
}

message ERequestV3 {
    string requestId = 1;
    repeated FDto f = 3;
    repeated string rIdFilter = 4;
}

message Service {
    string service = 1;
    string key = 2;
}

message FactDto {
    string factId = 1;                  
    google.protobuf.Any value = 2;
}

// This is a possible FDto value.
message FJsonValue {      
    string jsonValue = 1;               // The string with the JSON representation
    enum FType {                     // The supported types the JSON representation can be mapped to.
        STRING = 0;
        INTEGER = 1;
        BOOLEAN = 3;
        DOUBLE = 4;
        LIST_INTEGER = 5;
        LIST_STRING = 6;
        STATS_DATA = 7;
    }
    FType fType = 2;
}

message DeleteSessionRequest {
    string tenantId = 1;
    string entityId = 2;
}

message DeleteSessionResponse {
    bool success = 1;
}

Expected: Works the same as before, starts up fine with a Swagger UI.

Actual: Fails to start, gets lost in the gRPC generated code.

Workaround: Remove Kroto Plus, observe that it starts up again.

marcoferrer commented 4 years ago

Thanks for submitting this issue.

So I can better understand did this application instance always serve rest and grpc controllers and only broke once introducing kroto services?

Does the start up get hung up or is there an error written to console?

marcoferrer commented 4 years ago

So I don’t think the issue lies with spring specifically. I have a local project with kroto and spring working but without any rest controller or spring fox configured. Are you able to startup your application with the debug flag and see exactly what stage does the start up hang?

java -jar myapp.jar --debug

mattdkerr commented 4 years ago

the start-up hangs when Springfox Swagger2 starts scanning the parameters of the REST controller- I'm running using gradlew bootRun and passing --debug unfortunately looks like it turns on debugging logs for gradle instead of java

mattdkerr commented 4 years ago

So I can better understand did this application instance always serve rest and grpc controllers and only broke once introducing kroto services?

It always served REST and gRPC, and recently added Kroto. Then we added Swagger2 to both our services (the other one doesn't use Kroto yet).

Does the start up get hung up or is there an error written to console?

Without debug level it looks hung, but with it on, it's spewing stuff about walking gRPC classes and looks like it's in an infinite loop

marcoferrer commented 4 years ago

Can you try again but disabling except the proto builders generator. Im want to be able to isolate which generator is creating code that hangs up spring fox. My immediate guess it the proto builders generator, specifically because it is the only one creating resources used / scanned by spring fox

mattdkerr commented 4 years ago

Sorry, I haven't had a chance to try this yet. When I disabled the unpackBuilders option I realized I had to first remove the code that used it to even get back to where the error was. I'm just about done with that work, so I'll be able to cleanly add Kroto Plus back in afterwards in a test branch. Unfortunately it is lower priority than a few other tasks on my plate at work, so it may take a few days to get back to.

marcoferrer commented 4 years ago

Thanks for the help debugging the issue. When you do get a chance to get back to it, you can do a static import of the builder objects so you don’t have to update the usages in your code.