Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ or with inline pages:
}
```

**`BuilderJob` entity** (PostgreSQL, snake_case naming): `Id`, `SourceUri`, `SourceDataJson`, `Status`, `Created`, `Started`, `Finished`, `TotalPages`, `PagesCompleted`, `TotalWordCount`, `TotalImageCount`, `Errors`, `HangfireJobId`, `Services` (bitmask), `Title`, `CustomTypesJson`.
**`BuilderJob` entity** (PostgreSQL, snake_case naming): `Id`, `SourceUri`, `SourceDataJson`, `Status`, `Created`, `Started`, `Finished`, `TotalPages`, `PagesCompleted`, `TotalWordCount`, `TotalImageCount`, `Errors`, `HangfireJobId`, `Services` (bitmask), `Title`, `CustomTypesJson`, `InvocationCount`.

**`InvocationCount`**: tracks how many times the job processor has run for this job. Set to `1` on initial `POST`, incremented by 1 on every `PUT` (reprocess). Existing rows default to `1`. Returned on the `POST`/`PUT`/`GET` `JobResponse` and included in `JobCompletionNotification`, so callers can tell which run a completion notification belongs to. Server-managed only — not settable by the caller.

**`TextBuildJob`** (Hangfire): fetches manifest/resources concurrently (bounded by `MaxConcurrentPageFetches`), feeds `TextBuilder`, persists all artefacts, records per-page warnings without aborting the job.

Expand Down
7 changes: 7 additions & 0 deletions src/TextServices.Builder.Api/Data/BuilderJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,11 @@ public class BuilderJob
/// A value of 0 means processing completed but no derivatives could be produced.
/// </summary>
public int? FulfilledServices { get; set; }

/// <summary>
/// Number of times the job processor has been invoked for this job. Set to 1 on
/// initial creation (<c>POST</c>) and incremented by 1 on every reprocess (<c>PUT</c>),
/// so callers can tell which run a completion notification belongs to.
/// </summary>
public int InvocationCount { get; set; } = 1;
}
7 changes: 7 additions & 0 deletions src/TextServices.Builder.Api/Features/Jobs/JobResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class JobResponse
public int TotalImageCount { get; set; }
public string? Errors { get; set; }

/// <summary>
/// Number of times the job processor has been invoked for this job. 1 on initial
/// creation, incremented on every reprocess (<c>PUT</c>).
/// </summary>
public int InvocationCount { get; set; } = 1;

/// <summary>
/// The services that were actually produced during the most recent run.
/// Null for jobs processed before this field was introduced.
Expand Down Expand Up @@ -119,6 +125,7 @@ public static JobResponse From(BuilderJob job, TextServicesOptions options)
TotalWordCount = job.TotalWordCount,
TotalImageCount = job.TotalImageCount,
Errors = job.Errors,
InvocationCount = job.InvocationCount,
SearchV1 = searchV1,
AutocompleteV1 = autocompleteV1,
SearchV2 = searchV2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public async Task<ReprocessJobResult> Handle(ReprocessJobRequest request, Cancel
job.TotalImageCount = 0;
job.Errors = null;
job.HangfireJobId = null;
job.InvocationCount++;

await db.SaveChangesAsync(ct);

Expand Down
4 changes: 2 additions & 2 deletions src/TextServices.Builder.Api/Jobs/TextBuildJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public async Task ExecuteAsync(string jobId, IJobCancellationToken cancellationT

await jobNotifier.Notify(
new JobCompletionNotification(job.Id, job.Status, job.Finished,
job.TotalPages, job.TotalWordCount, job.Errors),
job.TotalPages, job.TotalWordCount, job.Errors, job.InvocationCount),
cancellationToken.ShutdownToken);
}
catch (Exception ex)
Expand All @@ -97,7 +97,7 @@ await jobNotifier.Notify(

await jobNotifier.Notify(
new JobCompletionNotification(job.Id, job.Status, job.Finished,
job.TotalPages, job.TotalWordCount, job.Errors),
job.TotalPages, job.TotalWordCount, job.Errors, job.InvocationCount),
cancellationToken.ShutdownToken);
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace TextServices.Builder.Api.Migrations
{
/// <inheritdoc />
public partial class AddInvocationCountToBuilderJob : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "invocation_count",
table: "jobs",
type: "integer",
nullable: false,
defaultValue: 1);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "invocation_count",
table: "jobs");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("hangfire_job_id");

b.Property<int>("InvocationCount")
.HasColumnType("integer")
.HasColumnName("invocation_count");

b.Property<int>("PagesCompleted")
.HasColumnType("integer")
.HasColumnName("pages_completed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public record JobCompletionNotification(
DateTimeOffset? Finished,
int TotalPages,
int TotalWordCount,
string? Errors);
string? Errors,
int InvocationCount);
1 change: 1 addition & 0 deletions src/TextServices.Demo/wwwroot/builder.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ <h2 style="margin:0">Jobs</h2>
<th>ID</th>
<th>Source</th>
<th>Status</th>
<th>Invocations</th>
<th>Progress</th>
<th>Words</th>
<th>Actions</th>
Expand Down
1 change: 1 addition & 0 deletions src/TextServices.Demo/wwwroot/js/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ function renderTable(rows) {
<td><code style="font-size:0.8rem">${esc(id)}</code></td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(sourceUri)}">${esc(truncate(sourceUri, 50))}</td>
<td><span class="status-badge status-${esc(status)}">${esc(status)}</span></td>
<td>${job?.invocationCount ?? '—'}</td>
<td>${renderProgress(job)}</td>
<td>${job?.totalWordCount != null ? job.totalWordCount.toLocaleString() : '—'}</td>
<td class="actions-cell"></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="AWSSDK.SecurityToken" Version="4.0.6.8" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.OpenApi" Version="2.9.0" />
Comment thread
JackLewis-digirati marked this conversation as resolved.
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" Version="2.9.0" />
</ItemGroup>
Expand Down
22 changes: 22 additions & 0 deletions src/TextServices.Tests/BuilderApi/JobResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ private static BuilderJob CompletedJob(
Status = JobStatus.Completed,
};

// -------------------------------------------------------------------------
// InvocationCount — reflects the job's current run count
// -------------------------------------------------------------------------

[Fact]
public void From_NewJob_InvocationCountIsOne()
{
var response = JobResponse.From(CompletedJob(), Options());
response.InvocationCount.ShouldBe(1);
}

[Fact]
public void From_ReprocessedJob_InvocationCountReflectsJobValue()
{
var job = CompletedJob();
job.InvocationCount = 3;

var response = JobResponse.From(job, Options());

response.InvocationCount.ShouldBe(3);
}

// -------------------------------------------------------------------------
// FulfilledServices field
// -------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/TextServices.Tests/BuilderApi/SnsJobNotifierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class SnsJobNotifierTests
private static JobCompletionNotification MakeNotification(
string jobId = "my/job",
JobStatus status = JobStatus.Completed) =>
new(jobId, status, DateTimeOffset.UtcNow, 10, 500, null);
new(jobId, status, DateTimeOffset.UtcNow, 10, 500, null, 1);

// -------------------------------------------------------------------------
// No-op when TopicArn is absent
Expand Down
29 changes: 28 additions & 1 deletion src/TextServices.Tests/BuilderApi/TextBuildJobTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,32 @@ public async Task ExecuteAsync_OnSuccess_NotifiesWithCompletedStatus()
notifier.Captured[0].Status.ShouldBe(JobStatus.Completed);
}

[Fact]
public async Task ExecuteAsync_OnSuccess_NotificationCarriesInvocationCount()
{
var pages = new List<PageInstruction>
{
new() { Id = "https://example.org/c/1", Width = 1000, Height = 1500,
TextUri = "https://example.org/alto/1.xml" },
};

var job = await CreateJob("test/notify-invocation-count",
sourceDataJson: JsonSerializer.Serialize(pages),
invocationCount: 2);

var altoFetcher = FakeAlto(new Dictionary<string, XElement>
{
["https://example.org/alto/1.xml"] = SampleAlto("hello world"),
});

var notifier = new CapturingJobNotifier();
var sut = MakeJob(altoFetcher: altoFetcher, jobNotifier: notifier);
await sut.ExecuteAsync(job.Id, FakeCancellationToken.Instance);

notifier.Captured.ShouldHaveSingleItem();
notifier.Captured[0].InvocationCount.ShouldBe(2);
}

[Fact]
public async Task ExecuteAsync_OnFailure_NotifiesWithFailedStatus()
{
Expand All @@ -622,14 +648,15 @@ public async Task ExecuteAsync_OnFailure_NotifiesWithFailedStatus()

private async Task<BuilderJob> CreateJob(string id,
string? sourceUri = null, string? sourceDataJson = null,
JobServices services = JobServices.All)
JobServices services = JobServices.All, int invocationCount = 1)
{
var job = new BuilderJob
{
Id = id,
SourceUri = sourceUri,
SourceDataJson = sourceDataJson,
Services = (int)services,
InvocationCount = invocationCount,
};
_db.Jobs.Add(job);
await _db.SaveChangesAsync();
Expand Down
Loading