alexei.dolgolyov 286b55986b perf(scraping): parallel HTTP fan-out, sequential DB persist (HIGH)
The Pull*UseCase implementations issued one HTTP request at a time despite
Scraping:MaxConcurrentRequests=4. With 30–80 live events and ~1s per
fetch, a 5–10s live cadence target was unreachable; cycles overflowed
the configured interval.

* New Marathon.Application.Configuration.ScrapingThrottle bound from the
  shared Scraping:* section. Exposes only MaxConcurrentRequests so the
  Application layer doesn't pull in the Infrastructure-side ScrapingOptions.
* PullLiveOddsUseCase + PullUpcomingEventsUseCase split into two phases:
  - Phase 1 — Parallel.ForEachAsync over the event list with
    MaxDegreeOfParallelism = throttle.MaxConcurrentRequests. The scraper's
    Polly rate limiter still throttles to RequestsPerSecond underneath
    this fan-out, so spikes are smoothed before they hit the bookmaker.
  - Phase 2 — sequential foreach over the (Event, Snapshot) tuples
    captured in Phase 1, doing event upsert + snapshot insert. EF Core
    DbContext is not thread-safe so all DB writes stay on a single thread.
* InfrastructureModule binds ScrapingThrottle alongside AnomalyOptions.
* Failed snapshot scrapes in Phase 1 mean the event row is also NOT
  persisted in Phase 2 — previously we'd persist the row even when the
  snapshot scrape failed, leaving an orphan event with no odds. Updated
  the regression test accordingly.
* Test fixture exposes TestFixtures.Throttle(maxConcurrentRequests=1) for
  deterministic sequential test runs.
* One existing NSubstitute setup that chained Arg.Is<>() across two
  configurations was rewritten to use a single Arg.Any<>() with inline
  branching — chained matchers were leaking and returning wrong results.
2026-05-09 15:27:06 +03:00

maraphon-app

Sports betting odds analyzer for marathonbet.by.

Scrapes pre-match (/su) and live (/su/live) sports events, tracks coefficient changes over time, and detects anomalies — in particular the "odds-flip" pattern where the bookmaker freezes betting and then inverts underdog/favorite odds.

Tech stack

  • .NET 8 + C# 12
  • Blazor Hybrid — WPF shell hosting BlazorWebView (designed to migrate to ASP.NET Core Blazor Server with no UI rewrite)
  • EF Core + SQLite (WAL mode) for local storage
  • ClosedXML for Excel export
  • AngleSharp for HTML scraping (with Playwright fallback for JS-rendered pages)
  • Polly v8 for retry / circuit breaker / rate limiting
  • MudBlazor UI components, Plotly.Blazor for charts
  • Serilog structured logging
  • xUnit / FluentAssertions / NSubstitute for tests

Project layout

src/
  Marathon.Domain/         entities, value objects, no dependencies
  Marathon.Application/    use cases, abstractions (IOddsScraper, IRepository, ...)
  Marathon.Infrastructure/ EF Core, scraping, Polly, Excel, Playwright
  Marathon.UI/             Razor Class Library — all Blazor components live here
  Marathon.Hosts.WpfBlazor/ WPF + BlazorWebView host (replaceable for web)
tests/
  Marathon.*.Tests/        unit + integration tests per layer

Build & run

dotnet build Marathon.sln
dotnet test  Marathon.sln
dotnet run   --project src/Marathon.Hosts.WpfBlazor

Configuration

All variable parameters (polling intervals, concurrency, user-agents, retry policy, snapshot retention, locale) are exposed via appsettings.json and live-editable via the in-app Settings page.

Status

🟡 In active development. See plans/initial-implementation/PLAN.md for the current phase plan and progress.

License

Private — customer project.

S
Description
No description provided
Readme 1.8 MiB
Languages
HTML 54.3%
C# 45%
CSS 0.7%