Skip to content

Implement mostRecent search type for rules historical condition#1138

Merged
timkimadobe merged 3 commits intoadobe:feature/historical-consequencefrom
timkimadobe:requalification-support
May 28, 2025
Merged

Implement mostRecent search type for rules historical condition#1138
timkimadobe merged 3 commits intoadobe:feature/historical-consequencefrom
timkimadobe:requalification-support

Conversation

@timkimadobe
Copy link
Copy Markdown
Contributor

@timkimadobe timkimadobe commented Apr 29, 2025

Description

This PR implements the mostRecent search type for rules historical condition, to support the requalification workflow.

The mostRecent search type is defined as follows:

The return type for this search is an integer. It represents the 0-based index of the most recent event, or -1 if none of the events are present in the event history.

The formats of the events parameter and its anonymous objects are the same as currently defined for historical conditions.

For example, given three distinct events in the events parameter: A, B, and C:

events: [A, B, C]

The system searches for all three events in the event history and checks which event is the most recent. For example, if B is the most recent event, the return value is 1. If A is the most recent event, the return value is 0.

A new Operand function called getMostRecentHistoricalEvent has been created to perform the most recent search, which requires as inputs:

  • runtime: ExtensionRuntime - to call getHistoricalEvents
  • requestEvents: [EventHistoryRequest] - to know which events to compare to return most recent

For each event provided in the condition, it searches event history and returns the index of the event with the newest occurrence, or -1 if none exist or a database error was encountered.

  • This early exit and return -1 is adopted from the existing getHistoricalEventCount implementation

Also adds new warnings for early exit or fallback paths in the related extractHistoricalCondition method

Alternative implementations

Considered potentially concurrently dispatching the getHistoricalEvents requests. Doesn't make sense for two reasons:

  1. First and foremost all database requests go through EventHistoryDatabase's serial queue, which means even if requests arrive concurrently, they are processed synchronously from the perspective of the database
  2. Even if the first point wasn't the case, it is an explosion of complexity for very little gain - currently the most complex use case for this logic is 2 events at most (qualify, unqualify)

Related Issue

Motivation and Context

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@timkimadobe timkimadobe force-pushed the requalification-support branch from 8c9fc24 to 5fed783 Compare April 30, 2025 00:47
let semaphore = DispatchSemaphore(value: 0)

runtime.getHistoricalEvents(requestEvents, enforceOrder: searchType == .ordered) { results in
defer { semaphore.signal() }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is unrelated to the mostRecent support, but I refactored the individual semaphore signals to use a defer instead in getHistoricalEventCount

Wondering if we should avoid making changes to what's not broken? (same question for the new params.count >= 3 to avoid index out of bounds errors (although based on the current code should be impossible))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'm with you on the "don't fix what's not broken" change. thoughts on both suggestions though:

  1. the defer looks like it's in the right spot and shouldn't change any functionality (our tests should confirm this!)
  2. i'm totally fine with a param count check on 205 - it doesn't change anything functionally but could serve as a fast fail if we don't have at least those 3 parameters we need.

similar to the comment made earlier in the review, i'm thinking that by using a switch statement inside of this callback we'd add some clarity to someone reading the code. what do you think about the idea of a little refactor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Doing the switch refactor really drove home the point that .mostRecent was being silently handled by the else case in the existing logic...

For this refactor, I made the following changes:

  • Added an early exit for the unsupported .mostRecent type to return the error value of -1 (since 0 is a valid and specific meaning for that search type)
  • Converted the nested if/else's into a switch handling all cases without a default
    • No default, so that any future cases will cause a compiler error and need to be explicitly handled (much unlike how adding .mostRecent had unnoticed potential side effects (although impossible with the current logic)
    • It does require a bit of redundancy to include the mostRecent here in the switch cases too, but I thought that the benefit of the compiler error point above outweighed this
  • The database error check has been turned into a guard instead

Please let me know what you think!

Comment on lines +90 to +95
init?(_ optionalValue: String?) {
guard let value = optionalValue, let enumValue = EventHistorySearchType(rawValue: value) else {
return nil
}
self = enumValue
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: talk me into this.

it feels unnecessary, as the rawValue initializer is effectively doing the same thing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As we discussed offline:

  • Removed this custom init which was basically adding a lot of unnecessary infrastructure just to handle unwrapping a String
  • Updated the search type to be even Swiftier 🤩 by using flatMap on the definition’s searchType, defaulting to .any when the value is either missing or fails to initialize a valid EventHistorySearchType

Comment on lines +184 to +190
let historyOperand: Operand<Int>
if searchType == .mostRecent {
historyOperand = Operand<Int>(function: getMostRecentHistoricalEvent, parameters: params)
} else {
// .any or .ordered
historyOperand = Operand<Int>(function: getHistoricalEventCount, parameters: params)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we're handling 3 different kinds of search types at this point - a more robust solution here would be to use a switch statement. it also makes it more clear to the reader (via code) how each case is handled, rather than depending on comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, thats a great point - updated to switch instead

let semaphore = DispatchSemaphore(value: 0)

runtime.getHistoricalEvents(requestEvents, enforceOrder: searchType == .ordered) { results in
defer { semaphore.signal() }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'm with you on the "don't fix what's not broken" change. thoughts on both suggestions though:

  1. the defer looks like it's in the right spot and shouldn't change any functionality (our tests should confirm this!)
  2. i'm totally fine with a param count check on 205 - it doesn't change anything functionally but could serve as a fast fail if we don't have at least those 3 parameters we need.

similar to the comment made earlier in the review, i'm thinking that by using a switch statement inside of this callback we'd add some clarity to someone reading the code. what do you think about the idea of a little refactor?

return returnValue
}

/// Returns the index of the most recent historical event based on occurrence date.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: date > timestamp

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you! Updated

runtime.getHistoricalEvents(requestEvents, enforceOrder: false) { eventResults in
defer { semaphore.signal() }

for (index, result) in eventResults.enumerated() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

clever 🤩

…emove default value logging

Replace search type operand handling with switch
Refactor getHistoricalEventCount with switch instead of nested if else and add early exit guard for unsupported type
Fix doc error
Copy link
Copy Markdown
Contributor Author

@timkimadobe timkimadobe left a comment

Choose a reason for hiding this comment

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

Thanks so much for the review @sbenedicadb! Updated based on feedback

Comment on lines +90 to +95
init?(_ optionalValue: String?) {
guard let value = optionalValue, let enumValue = EventHistorySearchType(rawValue: value) else {
return nil
}
self = enumValue
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As we discussed offline:

  • Removed this custom init which was basically adding a lot of unnecessary infrastructure just to handle unwrapping a String
  • Updated the search type to be even Swiftier 🤩 by using flatMap on the definition’s searchType, defaulting to .any when the value is either missing or fails to initialize a valid EventHistorySearchType

Comment on lines +184 to +190
let historyOperand: Operand<Int>
if searchType == .mostRecent {
historyOperand = Operand<Int>(function: getMostRecentHistoricalEvent, parameters: params)
} else {
// .any or .ordered
historyOperand = Operand<Int>(function: getHistoricalEventCount, parameters: params)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, thats a great point - updated to switch instead

let semaphore = DispatchSemaphore(value: 0)

runtime.getHistoricalEvents(requestEvents, enforceOrder: searchType == .ordered) { results in
defer { semaphore.signal() }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Doing the switch refactor really drove home the point that .mostRecent was being silently handled by the else case in the existing logic...

For this refactor, I made the following changes:

  • Added an early exit for the unsupported .mostRecent type to return the error value of -1 (since 0 is a valid and specific meaning for that search type)
  • Converted the nested if/else's into a switch handling all cases without a default
    • No default, so that any future cases will cause a compiler error and need to be explicitly handled (much unlike how adding .mostRecent had unnoticed potential side effects (although impossible with the current logic)
    • It does require a bit of redundancy to include the mostRecent here in the switch cases too, but I thought that the benefit of the compiler error point above outweighed this
  • The database error check has been turned into a guard instead

Please let me know what you think!

return returnValue
}

/// Returns the index of the most recent historical event based on occurrence date.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you! Updated

let runtime = params[0] as? ExtensionRuntime,
let requestEvents = params[1] as? [EventHistoryRequest],
let searchType = params[2] as? EventHistorySearchType else {
return 0
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also why does missing input values return 0? Isn't this an error case? Should getMostRecentHistoricalEvent not be returning -1 like it currently is in it's equivalent validation?

@timkimadobe timkimadobe requested a review from sbenedicadb May 6, 2025 18:32
@timkimadobe timkimadobe merged commit 87934f4 into adobe:feature/historical-consequence May 28, 2025
17 checks passed
@timkimadobe timkimadobe deleted the requalification-support branch May 28, 2025 23:36
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.

3 participants