Skip to content

Fix flakiness detection querying the security phase timeout - #1852

Open
hoco-scy wants to merge 18 commits into
masterfrom
fix/flakiness-phase-timeout
Open

hoco-scy wants to merge 18 commits into
masterfrom
fix/flakiness-phase-timeout

Conversation

@hoco-scy

@hoco-scy hoco-scy commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

FlakinessDetector.hasPhaseTimedOut() queries Phase.SECURITY instead of Phase.FLAKINESS.

ExecutionPhaseController retains timeout flags for completed phases. If the security phase has timed out, querying its flag returns true even after the flakiness phase starts with a fresh timer. Consequently, the timeout guards in static inference and re-execution can stop immediately, skipping flakiness detection.

Fix

Change the queried phase from SECURITY to FLAKINESS.

This is a one-line fix. It preserves the existing phase-wide time budget and does not add configuration options, change timeout policy, or modify result handling.

Validation

  • Existing EMConfigTest, RestCallResultTest, and FlakinessInferenceUtilTest passed.
  • Three additional local checks using the real ExecutionPhaseController passed:
    1. After a recorded security timeout, a newly started flakiness phase is not timed out. The original SECURITY query still returns true in this same state.
    2. After that transition, flakiness still times out when its own budget is exceeded.
    3. When security finishes without timing out, flakiness still respects its own budget.

The targeted checks simulated elapsed time by adjusting the controller's start timestamp; they were run locally and are not included in this minimal diff.

if (flakyList != null && flakyList.size <= i){
break
}
val flakyItem = if (flakyList != null) flakyList[i] else null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i equals flakyList.size, could accessing flakyList[i] here cause an index-out-of-bounds exception?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kotlin lists are zero-indexed, so flakyList[size] is out of bounds. Changing < to <= makes the loop break when i == flakyList.size, before reaching flakyList[i]. The previous condition missed this boundary case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this. If the sizes of merged and original differ, why do we return merged directly (its size may be greater or less than original)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 06ebbceb4c. The early return could miss element changes in later observations, so the merge now preserves the size difference while continuing to process shared indices.

For example, with original [1,2] and observations [1] then [9,2], the merged result is now [9] instead of [1].

Added regression tests for both observation orders, nested arrays, and generated assertions. All 68 related tests pass.

@man-zhang

Copy link
Copy Markdown
Contributor

@arcuri82 this PR is ready to review.
It supports configuring the budget percentage for flakiness handling and collecting statistics of the time spent on the re-execution for detecting flakiness. In addition, several bugs have been fixed.

@man-zhang
man-zhang requested a review from arcuri82 October 13, 2026 03:29
break
}
if (flakyList != null && flakyList.size < i){
if (flakyList != null && flakyList.size <= i){

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the i is from list, isn't it? shouldn't add at beggining of handleAssertionsOnList some explicit checks on the sizes of the two input data structures, and throw an IllegalArgumentException?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi, i is valid for list eg [1,2], but the flakyList may be shorter, eg, [3]. then in this case, the code is to ensure to avoid the IndexOutOfBoundsException when getting flakyList[i]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, could add a if(flakyList!=null && flakyList.size > list.size) throw IllegalArgumentException at the beginning of the function

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, it is more readable when the incrementing index is on left side, ie i >= flakyList.size

* @return whether the action is follow-up action
*/
fun isFollowUpAction(): Boolean{
return path.toString() == "/" && id.contains(HTTP_CALL_LOCATION_FOLLOWUP_KEY)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is path.toString() == "/" necessary? looks like hardcoding the placeholder from HttpSemanticService`

import org.evomaster.core.problem.rest.link.RestLink
import org.evomaster.core.problem.rest.param.PathParam
import org.evomaster.core.problem.rest.resource.ActionRToken
import org.evomaster.core.problem.rest.service.HttpSemanticsService.Companion.HTTP_CALL_LOCATION_FOLLOWUP_KEY

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this constant should be moved here in this class? as it might be used in different places in future, and then check forisFollowUpAction is here anyway


/**
* @return whether the action is follow-up action
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in docs, should explain what a follow-up action is. also, if the main problem is the lack of valid RestPath, then the name "follow-up" is confusing here. maybe something like isDirectLocationAction, explaining it is an action that alwasy use full Location from a previous response, and ignore its RestPath

val seq = seeMainExecutableActions()
while (cur >= 0){
val found = seq[cur]
if (!found.isFollowUpAction())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh? isn't this just returning the first previous non-followup action? that can be incorrect. as those actions are bound, you can use the links to refer to the source action in RestCallAction fields

/**
* for the follow-up action, get its previous valid rest action
*/
fun getActualPathForFollowUpAction(index: Int): RestPath?{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does Actual mean here? the name of this method feels confusing. isn't rather the path of the source of Location FUA depends on?

private fun updateParamInfo(action: RestCallAction, tables: Map<TableId, Table>) {
val r = rm.getResourceNodeFromCluster(action.path.toString())
private fun updateParamInfo(action: RestCallAction, path: RestPath?, tables: Map<TableId, Table>) {
val rPath = if (action.isFollowUpAction()) path else action.path

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the else, should throw exception if path!=null?

}

val budget = (config.timeLimitInSeconds() * config.extraPhaseBudgetPercentage).toLong()
val extraPercentage = if (target == Phase.FLAKINESS) config.extraBudgetPercentageForDetectFlakiness else config.extraPhaseBudgetPercentage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is confusing, as now extraPhaseBudgetPercentage is silently ignored for Flakiness. I would prefer something else like unboundBugetForFlakiness:Boolean where, if true, then no bduget checks are done for flakiness phase. false by default

internal fun getSqlInsertionExecutionTimeMs() = sqlInsertionExecutionTimeMs
internal fun getSqlInsertionExecutionCount() = sqlInsertionExecutionCount
internal fun getReExecutionTimesMs(): List<Long> = reExecutionTimesMs.toList()
internal fun getReExecutionTimeMs() = reExecutionTimesMs.sum()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those 2 names only differ with a single s in the middle. confusing. maybe rename first as getAllReExecutionTimesMs and second as getSumOfReExecutionTimesMs

@Cfg("Specify extra budget percentage of the search budget for flakiness detection phase." +
"A negative value indicates an unlimited time budget.")
@Min(-1.0)
var extraBudgetPercentageForDetectFlakiness = -1.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather use unboundBugetForFlakiness. also, if this is left as it is, it would mean flakiness phase was always unbound by default?

@man-zhang
man-zhang requested a review from arcuri82 October 18, 2026 05:37
break
}
if (flakyList != null && flakyList.size < i){
if (flakyList != null && flakyList.size <= i){

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should rather have i >= flakyList.size with a if(flakyList!=null && flakyList.size > list.size) throw IllegalArgumentException. also add javadocs to explain how flakyList relates to list

val locationId = seq[cur].usePreviousLocationId
cur = (cur - 1 downTo 0).firstOrNull {
val candidate = seq[it]
candidate.isPotentialActionForCreation() && candidate.creationLocationId() == locationId

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why checking for isPotentialActionForCreation ? technically a Location could be unrelated to create

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there is curently a limitation in the algorithm, could leave a TODO comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants