Skip to content

Commit 91c0519

Browse files
committed
Handle all failures in Future
1 parent 34606e3 commit 91c0519

File tree

5 files changed

+61
-24
lines changed

5 files changed

+61
-24
lines changed

bridge/src/main/scala/protocbridge/frontend/PluginFrontend.scala

+10-8
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,7 @@ object PluginFrontend {
4747
gen: ProtocCodeGenerator,
4848
request: Array[Byte]
4949
): Array[Byte] = {
50-
Try {
51-
gen.run(request)
52-
}.recover { case throwable =>
53-
createCodeGeneratorResponseWithError(
54-
throwable.toString + "\n" + getStackTrace(throwable)
55-
)
56-
}.get
50+
gen.run(request)
5751
}
5852

5953
def createCodeGeneratorResponseWithError(error: String): Array[Byte] = {
@@ -116,9 +110,17 @@ object PluginFrontend {
116110
gen: ProtocCodeGenerator,
117111
fsin: InputStream,
118112
env: ExtraEnv
119-
): Array[Byte] = {
113+
): Array[Byte] = try {
120114
val bytes = readInputStreamToByteArrayWithEnv(fsin, env)
121115
runWithBytes(gen, bytes)
116+
} catch {
117+
// This covers all Throwable including OutOfMemoryError, StackOverflowError, etc.
118+
// We need to make a best effort to return a response to protoc,
119+
// otherwise protoc can hang indefinitely.
120+
case throwable: Throwable =>
121+
createCodeGeneratorResponseWithError(
122+
throwable.toString + "\n" + getStackTrace(throwable)
123+
)
122124
}
123125

124126
def createTempFile(extension: String, content: String): Path = {

bridge/src/main/scala/protocbridge/frontend/PosixPluginFrontend.scala

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ object PosixPluginFrontend extends PluginFrontend {
4040
val response = PluginFrontend.runWithInputStream(plugin, fsin, env)
4141
fsin.close()
4242

43+
// Note that the output pipe must be opened after the input pipe is closed.
44+
// Otherwise, there might be a deadlock if the shell script is stuck in input pipe and
45+
// doesn't open the other end of the output pipe.
4346
val fsout = Files.newOutputStream(outputPipe)
4447
fsout.write(response)
4548
fsout.close()

bridge/src/test/scala/protocbridge/frontend/OsSpecificFrontendSpec.scala

+38-14
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,21 @@ import scala.util.Random
1111

1212
class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
1313

14-
protected def testPluginFrontend(frontend: PluginFrontend): Array[Byte] = {
15-
val random = new Random()
16-
val toSend = Array.fill(123)(random.nextInt(256).toByte)
17-
val toReceive = Array.fill(456)(random.nextInt(256).toByte)
18-
val env = new ExtraEnv(secondaryOutputDir = "tmp")
19-
20-
val fakeGenerator = new ProtocCodeGenerator {
21-
override def run(request: Array[Byte]): Array[Byte] = {
22-
request mustBe (toSend ++ env.toByteArrayAsField)
23-
toReceive
24-
}
25-
}
14+
protected def testPluginFrontend(
15+
frontend: PluginFrontend,
16+
generator: ProtocCodeGenerator,
17+
env: ExtraEnv,
18+
request: Array[Byte]
19+
): Array[Byte] = {
2620
val (path, state) = frontend.prepare(
27-
fakeGenerator,
21+
generator,
2822
env
2923
)
3024
val actualOutput = new ByteArrayOutputStream()
3125
val process = sys.process
3226
.Process(path.toAbsolutePath.toString)
3327
.run(new ProcessIO(writeInput => {
34-
writeInput.write(toSend)
28+
writeInput.write(request)
3529
writeInput.close()
3630
}, processOutput => {
3731
val buffer = new Array[Byte](4096)
@@ -48,4 +42,34 @@ class OsSpecificFrontendSpec extends AnyFlatSpec with Matchers {
4842
frontend.cleanup(state)
4943
actualOutput.toByteArray
5044
}
45+
46+
protected def testSuccess(frontend: PluginFrontend): Unit = {
47+
val random = new Random()
48+
val toSend = Array.fill(123)(random.nextInt(256).toByte)
49+
val toReceive = Array.fill(456)(random.nextInt(256).toByte)
50+
val env = new ExtraEnv(secondaryOutputDir = "tmp")
51+
52+
val fakeGenerator = new ProtocCodeGenerator {
53+
override def run(request: Array[Byte]): Array[Byte] = {
54+
request mustBe (toSend ++ env.toByteArrayAsField)
55+
toReceive
56+
}
57+
}
58+
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
59+
response mustBe toReceive
60+
}
61+
62+
protected def testFailure(frontend: PluginFrontend): Unit = {
63+
val random = new Random()
64+
val toSend = Array.fill(123)(random.nextInt(256).toByte)
65+
val env = new ExtraEnv(secondaryOutputDir = "tmp")
66+
67+
val fakeGenerator = new ProtocCodeGenerator {
68+
override def run(request: Array[Byte]): Array[Byte] = {
69+
throw new OutOfMemoryError("test error")
70+
}
71+
}
72+
val response = testPluginFrontend(frontend, fakeGenerator, env, toSend)
73+
response.length must be > 0
74+
}
5175
}

bridge/src/test/scala/protocbridge/frontend/PosixPluginFrontendSpec.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package protocbridge.frontend
33
class PosixPluginFrontendSpec extends OsSpecificFrontendSpec {
44
if (!PluginFrontend.isWindows) {
55
it must "execute a program that forwards input and output to given stream" in {
6-
testPluginFrontend(PosixPluginFrontend)
6+
testSuccess(PosixPluginFrontend)
7+
}
8+
9+
it must "not hang if there is an OOM in generator" in {
10+
testFailure(PosixPluginFrontend)
711
}
812
}
913
}

bridge/src/test/scala/protocbridge/frontend/WindowsPluginFrontendSpec.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package protocbridge.frontend
33
class WindowsPluginFrontendSpec extends OsSpecificFrontendSpec {
44
if (PluginFrontend.isWindows) {
55
it must "execute a program that forwards input and output to given stream" in {
6-
testPluginFrontend(WindowsPluginFrontend)
6+
testSuccess(WindowsPluginFrontend)
7+
}
8+
9+
it must "not hang if there is an OOM in generator" in {
10+
testFailure(WindowsPluginFrontend)
711
}
812
}
913
}

0 commit comments

Comments
 (0)