@@ -8,10 +8,8 @@ import (
88 "os"
99 "strings"
1010 "sync"
11- "time"
1211
1312 "github.com/LeanerCloud/CUDly/pkg/common"
14- "github.com/LeanerCloud/CUDly/pkg/provider"
1513 "github.com/LeanerCloud/CUDly/pkg/recfilter"
1614 "github.com/aws/aws-sdk-go-v2/aws"
1715 "github.com/aws/aws-sdk-go-v2/service/organizations"
@@ -20,11 +18,11 @@ import (
2018
2119// Constants for purchase processing.
2220const (
23- // DefaultDuplicateCheckLookbackHours is the default lookback period for checking recent purchases.
24- DefaultDuplicateCheckLookbackHours = 24
25-
2621 // PurchaseDelaySeconds is the delay between consecutive purchases to avoid rate limiting.
2722 PurchaseDelaySeconds = 2
23+
24+ // DefaultDuplicateCheckLookbackHours is re-exported from pkg/recfilter.
25+ DefaultDuplicateCheckLookbackHours = recfilter .DefaultDuplicateCheckLookbackHours
2826)
2927
3028// AppLogger is a simple logger for application output.
@@ -234,136 +232,17 @@ func ConfirmPurchase(totalInstances int, totalSavings float64, skipConfirmation
234232// and tests are unchanged.
235233func CheckAuditLogWritable (path string ) error { return common .CheckAuditLogWritable (path ) }
236234
237- // DuplicateChecker checks for existing commitments to avoid duplicates.
238- type DuplicateChecker struct {
239- LookbackHours int // How many hours to look back for recent purchases
240- }
235+ // DuplicateChecker is re-exported from pkg/recfilter so cmd's existing call
236+ // sites and tests are unchanged.
237+ type DuplicateChecker = recfilter.DuplicateChecker
241238
242- // NewDuplicateChecker creates a new duplicate checker. Pass 0 to use the default lookback period.
239+ // NewDuplicateChecker creates a new duplicate checker. Pass 0 to use the
240+ // default lookback period. Logf is wired to log.Printf so the CLI's
241+ // decision trail keeps going to stderr exactly as it does today.
243242func NewDuplicateChecker (hours int ) * DuplicateChecker {
244- if hours <= 0 {
245- hours = DefaultDuplicateCheckLookbackHours
246- }
247- return & DuplicateChecker {
248- LookbackHours : hours ,
249- }
250- }
251-
252- // AdjustRecommendationsForExisting adjusts recommendations based on existing commitments
253- // This checks for recently purchased RIs (within LookbackHours) to avoid duplicate purchases.
254- // Note: This is designed to prevent re-purchasing something you just bought, not to prevent
255- // purchasing RIs in other accounts that happen to have the same characteristics.
256- func (d * DuplicateChecker ) AdjustRecommendationsForExisting (ctx context.Context , recs []common.Recommendation , client provider.ServiceClient ) (passed , filtered []common.Recommendation , err error ) {
257- existing , err := client .GetExistingCommitments (ctx )
258- if err != nil {
259- return recs , nil , err
260- }
261-
262- log .Printf (" [DuplicateChecker] Found %d total existing commitments" , len (existing ))
263-
264- recentExisting := d .filterRecentCommitments (existing )
265- log .Printf (" [DuplicateChecker] Found %d recent commitments (purchased in last %d hours)" , len (recentExisting ), d .LookbackHours )
266-
267- if len (recentExisting ) == 0 {
268- return recs , nil , nil
269- }
270-
271- existingMap := buildExistingCommitmentsMap (recentExisting )
272- log .Printf (" [DuplicateChecker] Existing map has %d unique keys" , len (existingMap ))
273-
274- passed , filtered = adjustRecommendationsAgainstExisting (recs , existingMap )
275-
276- if len (filtered ) > 0 {
277- log .Printf (" [DuplicateChecker] Result: %d recommendations kept out of %d (avoided %d duplicates)" ,
278- len (passed ), len (recs ), len (filtered ))
279- }
280- return passed , filtered , nil
281- }
282-
283- // filterRecentCommitments filters commitments to only recent purchases within the lookback window.
284- func (d * DuplicateChecker ) filterRecentCommitments (existing []common.Commitment ) []common.Commitment {
285- cutoffTime := time .Now ().Add (- time .Duration (d .LookbackHours ) * time .Hour )
286- recentExisting := make ([]common.Commitment , 0 )
287-
288- for _rvc := range existing {
289- c := existing [_rvc ]
290- if isRecentActiveCommitment (c , cutoffTime ) {
291- recentExisting = append (recentExisting , c )
292- }
293- }
294-
295- return recentExisting
296- }
297-
298- // isRecentActiveCommitment checks if a commitment is active and purchased after the cutoff time.
299- func isRecentActiveCommitment (c common.Commitment , cutoffTime time.Time ) bool {
300- return (c .State == "active" || c .State == "payment-pending" ) && c .StartDate .After (cutoffTime )
301- }
302-
303- // buildExistingCommitmentsMap builds a map of commitments by resource type, region, and engine.
304- func buildExistingCommitmentsMap (commitments []common.Commitment ) map [string ]int {
305- existingMap := make (map [string ]int )
306-
307- for _rvc := range commitments {
308- c := commitments [_rvc ]
309- normalizedEngine := common .NormalizeEngineName (c .Engine )
310- key := fmt .Sprintf ("%s|%s|%s" , c .ResourceType , c .Region , normalizedEngine )
311- existingMap [key ] += c .Count
312- log .Printf (" [DuplicateChecker] Recent RI: key=%s count=%d startDate=%s (raw engine=%s)" ,
313- key , c .Count , c .StartDate .Format ("2006-01-02 15:04:05" ), c .Engine )
314- }
315-
316- return existingMap
317- }
318-
319- // adjustRecommendationsAgainstExisting adjusts recommendations based on existing commitments.
320- // Returns (passed, filtered) where filtered contains recs whose count was reduced to zero.
321- func adjustRecommendationsAgainstExisting (recs []common.Recommendation , existingMap map [string ]int ) (passed , filtered []common.Recommendation ) {
322- passed = make ([]common.Recommendation , 0 , len (recs ))
323- filtered = make ([]common.Recommendation , 0 )
324-
325- for _rvc := range recs {
326- rec := recs [_rvc ]
327- adjusted := adjustSingleRecommendation (rec , existingMap )
328- if adjusted .Count > 0 {
329- passed = append (passed , adjusted )
330- } else {
331- filtered = append (filtered , rec )
332- }
333- }
334-
335- return passed , filtered
336- }
337-
338- // adjustSingleRecommendation adjusts a single recommendation based on existing commitments.
339- func adjustSingleRecommendation (rec common.Recommendation , existingMap map [string ]int ) common.Recommendation {
340- engine := common .EngineFromDetails (rec .Details )
341- key := fmt .Sprintf ("%s|%s|%s" , rec .ResourceType , rec .Region , engine )
342- existingCount := existingMap [key ]
343-
344- if existingCount >= rec .Count {
345- // All of this recommendation is covered by recent RIs.
346- // Return a zero-value Recommendation (Count=0) as a sentinel; the caller
347- // (adjustRecommendationsAgainstExisting) filters out recommendations with Count <= 0.
348- log .Printf (" [DuplicateChecker] SKIP %s: recent %d >= recommended %d" , key , existingCount , rec .Count )
349- existingMap [key ] -= rec .Count
350- return common.Recommendation {Count : 0 }
351- }
352-
353- // Partial or no coverage by recent RIs
354- adjusted := rec
355- if existingCount > 0 {
356- adjusted .Count = rec .Count - existingCount
357- existingMap [key ] = 0
358- log .Printf (" [DuplicateChecker] PARTIAL %s: adjusted count from %d to %d" , key , rec .Count , adjusted .Count )
359- }
360-
361- return adjusted
362- }
363-
364- // AdjustRecommendationsForExistingRIs is an alias for AdjustRecommendationsForExisting.
365- func (d * DuplicateChecker ) AdjustRecommendationsForExistingRIs (ctx context.Context , recs []common.Recommendation , client provider.ServiceClient ) (passed , filtered []common.Recommendation , err error ) {
366- return d .AdjustRecommendationsForExisting (ctx , recs , client )
243+ d := recfilter .NewDuplicateChecker (hours )
244+ d .Logf = log .Printf
245+ return d
367246}
368247
369248// GetRecommendationDescription returns a human-readable description.
0 commit comments