Skip to content

Commit 7bce1ee

Browse files
committed
refactor(recfilter): extract DuplicateChecker to pkg
The 24h recent-purchase guard lived in package main, so the MCP server had no way to see capacity the CLI bought minutes earlier. Today an MCP purchase can land on top of a CLI purchase 10 minutes old; wiring that guard in needs the checker importable first. DuplicateChecker, its helpers and DefaultDuplicateCheckLookbackHours move into pkg/recfilter/dedupe.go. The decision trail routes through an injected Logf so the MCP server can run the checker silently; cmd's NewDuplicateChecker wires log.Printf, keeping the CLI's stderr output unchanged. cmd re-exports DuplicateChecker as a type alias (not a defined type, so method calls still resolve) and DefaultDuplicateCheckLookbackHours as a const, leaving its existing tests untouched. Refs #1883
1 parent 1b9e8da commit 7bce1ee

3 files changed

Lines changed: 372 additions & 133 deletions

File tree

cmd/helpers.go

Lines changed: 12 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2220
const (
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.
235233
func 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.
243242
func 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.

pkg/recfilter/dedupe.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package recfilter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/LeanerCloud/CUDly/pkg/common"
9+
"github.com/LeanerCloud/CUDly/pkg/provider"
10+
)
11+
12+
// DefaultDuplicateCheckLookbackHours is the default lookback period for checking recent purchases.
13+
const DefaultDuplicateCheckLookbackHours = 24
14+
15+
// DuplicateChecker checks for existing commitments to avoid duplicates.
16+
type DuplicateChecker struct {
17+
LookbackHours int // How many hours to look back for recent purchases
18+
19+
// Logf receives the per-commitment decision trail. Nil is silent.
20+
// recfilter never logs through a package-level logger: cmd's AppLogger
21+
// writes to stdout, which the MCP server owns as its protocol transport.
22+
Logf Logf
23+
}
24+
25+
// NewDuplicateChecker creates a new duplicate checker. Pass 0 to use the default lookback period.
26+
func NewDuplicateChecker(hours int) *DuplicateChecker {
27+
if hours <= 0 {
28+
hours = DefaultDuplicateCheckLookbackHours
29+
}
30+
return &DuplicateChecker{
31+
LookbackHours: hours,
32+
}
33+
}
34+
35+
// AdjustRecommendationsForExisting adjusts recommendations based on existing commitments
36+
// This checks for recently purchased RIs (within LookbackHours) to avoid duplicate purchases.
37+
// Note: This is designed to prevent re-purchasing something you just bought, not to prevent
38+
// purchasing RIs in other accounts that happen to have the same characteristics.
39+
func (d *DuplicateChecker) AdjustRecommendationsForExisting(ctx context.Context, recs []common.Recommendation, client provider.ServiceClient) (passed, filtered []common.Recommendation, err error) {
40+
existing, err := client.GetExistingCommitments(ctx)
41+
if err != nil {
42+
return recs, nil, err
43+
}
44+
45+
d.Logf.printf(" [DuplicateChecker] Found %d total existing commitments", len(existing))
46+
47+
recentExisting := d.filterRecentCommitments(existing)
48+
d.Logf.printf(" [DuplicateChecker] Found %d recent commitments (purchased in last %d hours)", len(recentExisting), d.LookbackHours)
49+
50+
if len(recentExisting) == 0 {
51+
return recs, nil, nil
52+
}
53+
54+
existingMap := buildExistingCommitmentsMap(recentExisting, d.Logf)
55+
d.Logf.printf(" [DuplicateChecker] Existing map has %d unique keys", len(existingMap))
56+
57+
passed, filtered = adjustRecommendationsAgainstExisting(recs, existingMap, d.Logf)
58+
59+
if len(filtered) > 0 {
60+
d.Logf.printf(" [DuplicateChecker] Result: %d recommendations kept out of %d (avoided %d duplicates)",
61+
len(passed), len(recs), len(filtered))
62+
}
63+
return passed, filtered, nil
64+
}
65+
66+
// filterRecentCommitments filters commitments to only recent purchases within the lookback window.
67+
func (d *DuplicateChecker) filterRecentCommitments(existing []common.Commitment) []common.Commitment {
68+
cutoffTime := time.Now().Add(-time.Duration(d.LookbackHours) * time.Hour)
69+
recentExisting := make([]common.Commitment, 0)
70+
71+
for _rvc := range existing {
72+
c := existing[_rvc]
73+
if isRecentActiveCommitment(c, cutoffTime) {
74+
recentExisting = append(recentExisting, c)
75+
}
76+
}
77+
78+
return recentExisting
79+
}
80+
81+
// isRecentActiveCommitment checks if a commitment is active and purchased after the cutoff time.
82+
func isRecentActiveCommitment(c common.Commitment, cutoffTime time.Time) bool {
83+
return (c.State == "active" || c.State == "payment-pending") && c.StartDate.After(cutoffTime)
84+
}
85+
86+
// buildExistingCommitmentsMap builds a map of commitments by resource type, region, and engine.
87+
func buildExistingCommitmentsMap(commitments []common.Commitment, logf Logf) map[string]int {
88+
existingMap := make(map[string]int)
89+
90+
for _rvc := range commitments {
91+
c := commitments[_rvc]
92+
normalizedEngine := common.NormalizeEngineName(c.Engine)
93+
key := fmt.Sprintf("%s|%s|%s", c.ResourceType, c.Region, normalizedEngine)
94+
existingMap[key] += c.Count
95+
logf.printf(" [DuplicateChecker] Recent RI: key=%s count=%d startDate=%s (raw engine=%s)",
96+
key, c.Count, c.StartDate.Format("2006-01-02 15:04:05"), c.Engine)
97+
}
98+
99+
return existingMap
100+
}
101+
102+
// adjustRecommendationsAgainstExisting adjusts recommendations based on existing commitments.
103+
// Returns (passed, filtered) where filtered contains recs whose count was reduced to zero.
104+
func adjustRecommendationsAgainstExisting(recs []common.Recommendation, existingMap map[string]int, logf Logf) (passed, filtered []common.Recommendation) {
105+
passed = make([]common.Recommendation, 0, len(recs))
106+
filtered = make([]common.Recommendation, 0)
107+
108+
for _rvc := range recs {
109+
rec := recs[_rvc]
110+
adjusted := adjustSingleRecommendation(rec, existingMap, logf)
111+
if adjusted.Count > 0 {
112+
passed = append(passed, adjusted)
113+
} else {
114+
filtered = append(filtered, rec)
115+
}
116+
}
117+
118+
return passed, filtered
119+
}
120+
121+
// adjustSingleRecommendation adjusts a single recommendation based on existing commitments.
122+
func adjustSingleRecommendation(rec common.Recommendation, existingMap map[string]int, logf Logf) common.Recommendation {
123+
engine := common.EngineFromDetails(rec.Details)
124+
key := fmt.Sprintf("%s|%s|%s", rec.ResourceType, rec.Region, engine)
125+
existingCount := existingMap[key]
126+
127+
if existingCount >= rec.Count {
128+
// All of this recommendation is covered by recent RIs.
129+
// Return a zero-value Recommendation (Count=0) as a sentinel; the caller
130+
// (adjustRecommendationsAgainstExisting) filters out recommendations with Count <= 0.
131+
logf.printf(" [DuplicateChecker] SKIP %s: recent %d >= recommended %d", key, existingCount, rec.Count)
132+
existingMap[key] -= rec.Count
133+
return common.Recommendation{Count: 0}
134+
}
135+
136+
// Partial or no coverage by recent RIs
137+
adjusted := rec
138+
if existingCount > 0 {
139+
adjusted.Count = rec.Count - existingCount
140+
existingMap[key] = 0
141+
logf.printf(" [DuplicateChecker] PARTIAL %s: adjusted count from %d to %d", key, rec.Count, adjusted.Count)
142+
}
143+
144+
return adjusted
145+
}
146+
147+
// AdjustRecommendationsForExistingRIs is an alias for AdjustRecommendationsForExisting.
148+
func (d *DuplicateChecker) AdjustRecommendationsForExistingRIs(ctx context.Context, recs []common.Recommendation, client provider.ServiceClient) (passed, filtered []common.Recommendation, err error) {
149+
return d.AdjustRecommendationsForExisting(ctx, recs, client)
150+
}

0 commit comments

Comments
 (0)