Skip to content

Commit 0def519

Browse files
enbntjenkins
authored and
jenkins
committed
util-app: roll up flag parsing errors
Problem When parsing flags, we escape on the first parsing failure. This behavior results in poor user experience and odd side effects in certain scenarios. For the user, if they have multiple flag errors, they are forced to iterate and fix the errors 1-by-1. Without knowing that there are other parse failures to fix - this is a slow and time consuming feedback loop. In instances where an app references flag values as part of the lifecycle (i.e. as part of cleaning up/closing the app), a parse failure can result in remaining flags not being parsed, which cascades when we fail when flags are referenced, but not yet parsed, resulting in unclean shutdown of the app. Solution We will ensure that all flags get parsed consistently and that we roll-up errors as a single error result. We will also print a help message as part of a parse error, so that users are given the reason their flags were failed to be parsed, along with the expected usage for defined/available flags. Result A more streamlined experience for users attempting to remedy mistaken flag definitions and better behavior when flags are referenced during various phases of the application lifecycle. JIRA Issues: CSL-11231 Differential Revision: https://phabricator.twitter.biz/D729700
1 parent a9afb22 commit 0def519

File tree

3 files changed

+31
-10
lines changed

3 files changed

+31
-10
lines changed

Diff for: CHANGELOG.rst

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ Note that ``PHAB_ID=#`` and ``RB_ID=#`` correspond to associated messages in com
66

77
Unreleased
88
----------
9+
910
New Features
1011
~~~~~~~~~~~~
1112

1213
* util-stats: Counter, Gauge, and Stat can be instrumented with descriptions. ``PHAB_ID = D615481``
1314

14-
New Features
15-
~~~~~~~~~~~~
16-
1715
* util-cache: Experimentally crossbuilds with Scala 3. ``PHAB_ID=D714304``.
1816

1917
* util-cache-guava: Experimentally crossbuilds with Scala 3. ``PHAB_ID=D716101``.
@@ -28,6 +26,12 @@ New Features
2826

2927
* util-zk-test: Experimentally crossbuilds with Scala 3. ``PHAB_ID=D720603``
3028

29+
* util-app: Flags parsing will now roll-up multiple flag parsing errors into a single
30+
error message. When an error is encountered, flag parsing will continue to collect parse error
31+
information instead of escaping on the first flag failure. After parsing all flags, if any errors
32+
are present, a message containing all of the failed flags and their error reason,
33+
along with the `help` usage message will be emitted. ```PHAB_ID=D729700``
34+
3135
Runtime Behavior Changes
3236
~~~~~~~~~~~~~~~~~~~~~~~~
3337

Diff for: util-app/src/main/scala/com/twitter/app/Flags.scala

+12-7
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ object Flags {
4949
*
5050
* @param reason A string explaining the error that occurred.
5151
*/
52-
case class Error(reason: String) extends FlagParseResult
52+
case class Error(reason: String) extends FlagParseResult {
53+
override def toString: String = reason
54+
}
5355
}
5456

5557
/**
@@ -128,6 +130,7 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
128130
synchronized {
129131
reset()
130132
val remaining = new ArrayBuffer[String]
133+
val errors = new ArrayBuffer[Error]
131134
var i = 0
132135
while (i < args.length) {
133136
val a = args(i)
@@ -144,7 +147,7 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
144147
if (allowUndefinedFlags)
145148
remaining += a
146149
else
147-
return Error(
150+
errors += Error(
148151
"Error parsing flag \"%s\": %s".format(k, FlagUndefinedMessage)
149152
)
150153

@@ -153,7 +156,7 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
153156
if (allowUndefinedFlags)
154157
remaining += a
155158
else
156-
return Error(
159+
errors += Error(
157160
"Error parsing flag \"%s\": %s".format(k, FlagUndefinedMessage)
158161
)
159162

@@ -163,7 +166,7 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
163166

164167
// Mandatory argument without a value and with no more arguments.
165168
case Array(k) if i == args.length =>
166-
return Error(
169+
errors += Error(
167170
"Error parsing flag \"%s\": %s".format(k, FlagValueRequiredMessage)
168171
)
169172

@@ -173,7 +176,7 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
173176
try flag(k).parse(args(i - 1))
174177
catch {
175178
case NonFatal(e) =>
176-
return Error(
179+
errors += Error(
177180
"Error parsing flag \"%s\": %s".format(k, e.getMessage)
178181
)
179182
}
@@ -182,8 +185,8 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
182185
case Array(k, v) =>
183186
try flag(k).parse(v)
184187
catch {
185-
case e: Throwable =>
186-
return Error(
188+
case NonFatal(e) =>
189+
errors += Error(
187190
"Error parsing flag \"%s\": %s".format(k, e.getMessage)
188191
)
189192
}
@@ -196,6 +199,8 @@ final class Flags(argv0: String, includeGlobal: Boolean, failFastUntilParsed: Bo
196199

197200
if (helpFlag())
198201
Help(usage)
202+
else if (errors.nonEmpty)
203+
Error(s"Error parsing flags: ${errors.mkString("[\n ", ",\n ", "\n]")}\n\n$usage")
199204
else
200205
Ok(remaining.toSeq)
201206
}

Diff for: util-app/src/test/scala/com/twitter/app/FlagsTest.scala

+12
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ class FlagsTest extends AnyFunSuite {
165165
)
166166
}
167167

168+
test("Flag: multiple parse errors roll-up") {
169+
val ctx = new Ctx
170+
import ctx._
171+
flag.parseArgs(Array("-undefined", "-foo", "blah")) match {
172+
case Flags.Error(reason) =>
173+
assert(reason.contains("Error parsing flag \"undefined\": flag undefined"))
174+
assert(reason.contains("Error parsing flag \"foo\": For input string: \"blah\""))
175+
assert(reason.contains("usage:"))
176+
case other => fail(s"expected a flag error, but received $other")
177+
}
178+
}
179+
168180
test("formatFlagValues") {
169181

170182
val flagWithGlobal = new Flags("my", includeGlobal = true)

0 commit comments

Comments
 (0)