Skip to content
Tracon

JobStoreContract

Namespace Tracon.Testing.Contracts.Storage · Assembly Tracon.Testing.Contracts.Xunit.dll

Behavior tests for the IJobStore contract.

public abstract class JobStoreContract : TenantIsolationContract<IJobStore>, IAsyncLifetime, IAsyncDisposable
Inheritance
object ← StoreCancellationContract<IJobStore> ← TenantIsolationContract<IJobStore> ← JobStoreContract
Implements
IAsyncLifetime, IAsyncDisposable
Inherited Members
TenantIsolationContract<IJobStore>.TenantA, TenantIsolationContract<IJobStore>.TenantB, TenantIsolationContract<IJobStore>.UseAmbientTenant(MutableTenantContext), TenantIsolationContract<IJobStore>.SeedAsync(string, string, CancellationToken), TenantIsolationContract<IJobStore>.ExistsAsync(string, object), TenantIsolationContract<IJobStore>.CountAsync(string, CancellationToken), TenantIsolationContract<IJobStore>.TryDeleteAsync(string, object), TenantIsolationContract<IJobStore>.TryOverwriteAsync(string, string), TenantIsolationContract<IJobStore>.CancellableReadAsync(CancellationToken), TenantIsolationContract<IJobStore>.CancellableWriteAsync(CancellationToken), TenantIsolationContract<IJobStore>.WroteAnythingAsync(), TenantIsolationContract<IJobStore>.Tenant_cannot_read_another_tenants_record(), TenantIsolationContract<IJobStore>.Tenant_reads_its_own_record(), TenantIsolationContract<IJobStore>.Tenant_does_not_see_another_tenants_record_in_the_list(), TenantIsolationContract<IJobStore>.Tenant_cannot_delete_another_tenants_record(), TenantIsolationContract<IJobStore>.Same_name_lives_independently_across_two_tenants(), TenantIsolationContract<IJobStore>.AmbientTenant, StoreCancellationContract<IJobStore>.CreateStoreAsync(), StoreCancellationContract<IJobStore>.InitializeAsync(), StoreCancellationContract<IJobStore>.DisposeAsync(), StoreCancellationContract<IJobStore>.OnDisposeAsync(), StoreCancellationContract<IJobStore>.CancellableReadAsync(CancellationToken), StoreCancellationContract<IJobStore>.CancellableWriteAsync(CancellationToken), StoreCancellationContract<IJobStore>.WroteAnythingAsync(), StoreCancellationContract<IJobStore>.Canceled_token_throws_on_read(), StoreCancellationContract<IJobStore>.The_write_this_contract_checks_can_be_read_back(), StoreCancellationContract<IJobStore>.Canceled_token_throws_on_write_and_leaves_no_trace(), StoreCancellationContract<IJobStore>.Store, object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

The in-memory store and the PostgreSQL store must pass the same scenarios. The lease-expiry test runs against real time (a short lease + a short wait): the PostgreSQL implementation does not take its clock through an injectable TimeProvider, so fake time cannot be used here.

protected JobStoreContract()

A_cancelled_job_is_changed_by_neither_completion_nor_release()

Section titled “ A_cancelled_job_is_changed_by_neither_completion_nor_release()”
[Fact]
public Task A_cancelled_job_is_changed_by_neither_completion_nor_release()

Task

A_finished_job_is_not_finalized_a_second_time()

Section titled “ A_finished_job_is_not_finalized_a_second_time()”
[Fact]
public Task A_finished_job_is_not_finalized_a_second_time()

Task

A_job_without_a_time_limit_reads_back_none()

Section titled “ A_job_without_a_time_limit_reads_back_none()”
[Fact]
public Task A_job_without_a_time_limit_reads_back_none()

Task

A_jobs_own_time_limit_survives_the_round_trip_and_the_lease()

Section titled “ A_jobs_own_time_limit_survives_the_round_trip_and_the_lease()”
[Fact]
public Task A_jobs_own_time_limit_survives_the_round_trip_and_the_lease()

Task

A_jobs_time_limit_beyond_an_int_of_milliseconds_survives_the_round_trip_and_the_lease()

Section titled “ A_jobs_time_limit_beyond_an_int_of_milliseconds_survives_the_round_trip_and_the_lease()”
[Fact]
public Task A_jobs_time_limit_beyond_an_int_of_milliseconds_survives_the_round_trip_and_the_lease()

Task

A_pending_report_marks_the_item_and_counts_nothing()

Section titled “ A_pending_report_marks_the_item_and_counts_nothing()”

A pending report marks the item with the run that is processing it and counts nothing; the report that finishes the item is counted once.

[Fact]
public Task A_pending_report_marks_the_item_and_counts_nothing()

Task

Workflow recovery writes the run id on the item BEFORE the run starts, so a job leased again after a crash finds the run the item was processing. A store that counted the marker, or finished the item on it, would close the job as done while its work was still in flight.

A_report_for_a_finished_item_changes_nothing()

Section titled “ A_report_for_a_finished_item_changes_nothing()”

A report for an item that already finished changes neither the item nor the counters.

[Fact]
public Task A_report_for_a_finished_item_changes_nothing()

Task

The SQL stores update an item only while it is pending. A store that overwrote a finished item let a late report from a worker whose lease had expired rewrite the result the job already gave, and a marker written after the item finished would turn it back into a pending item.

An_id_that_is_already_queued_is_refused_and_the_queued_job_is_kept()

Section titled “ An_id_that_is_already_queued_is_refused_and_the_queued_job_is_kept()”
[Fact]
public Task An_id_that_is_already_queued_is_refused_and_the_queued_job_is_kept()

Task

CancelAsync_does_not_affect_another_tenants_job()

Section titled “ CancelAsync_does_not_affect_another_tenants_job()”
[Fact]
public Task CancelAsync_does_not_affect_another_tenants_job()

Task

CancelAsync_works_only_in_eligible_states(JobStatus, bool)

Section titled “ CancelAsync_works_only_in_eligible_states(JobStatus, bool)”
[Theory]
[InlineData(new object?[] { JobStatus.Pending, true })]
[InlineData(new object?[] { JobStatus.Completed, false })]
[InlineData(new object?[] { JobStatus.Failed, false })]
public Task CancelAsync_works_only_in_eligible_states(JobStatus status, bool expected)

status JobStatus

expected bool

Task

[Fact]
public Task CompleteAsync_releases_the_lease()

Task

Reports how many records the given tenant sees through the listing endpoint.

protected override ValueTask<int> CountAsync(string tenantId, CancellationToken cancellationToken)

tenantId string

The reading tenant.

cancellationToken CancellationToken

The token every store call inside the hook must be handed; see TenantIsolationContract.SeedAsync.

ValueTask<int>

The number of records seen.

Enqueued_job_defaults_to_the_default_lane()

Section titled “ Enqueued_job_defaults_to_the_default_lane()”
[Fact]
public Task Enqueued_job_defaults_to_the_default_lane()

Task

Enqueued_job_starts_pending_and_creates_items()

Section titled “ Enqueued_job_starts_pending_and_creates_items()”
[Fact]
public Task Enqueued_job_starts_pending_and_creates_items()

Task

Reports whether the record is visible from the given tenant’s viewpoint.

protected override ValueTask<bool> ExistsAsync(string tenantId, object key)

tenantId string

The reading tenant.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool>

true if the record is visible.

GetAsync_returns_null_for_another_tenant()

Section titled “ GetAsync_returns_null_for_another_tenant()”
[Fact]
public Task GetAsync_returns_null_for_another_tenant()

Task

Job_can_be_re_leased_once_the_lease_expires()

Section titled “ Job_can_be_re_leased_once_the_lease_expires()”
[Fact]
public Task Job_can_be_re_leased_once_the_lease_expires()

Task

[Fact]
public Task Job_not_yet_due_cannot_be_leased()

Task

Lease_with_a_lane_filter_never_returns_a_job_from_an_unlisted_lane()

Section titled “ Lease_with_a_lane_filter_never_returns_a_job_from_an_unlisted_lane()”
[Fact]
public Task Lease_with_a_lane_filter_never_returns_a_job_from_an_unlisted_lane()

Task

Lease_with_a_lane_filter_only_returns_a_job_in_that_lane()

Section titled “ Lease_with_a_lane_filter_only_returns_a_job_in_that_lane()”
[Fact]
public Task Lease_with_a_lane_filter_only_returns_a_job_in_that_lane()

Task

Lease_with_a_tenant_cap_counts_a_running_job()

Section titled “ Lease_with_a_tenant_cap_counts_a_running_job()”
[Fact]
public Task Lease_with_a_tenant_cap_counts_a_running_job()

Task

Lease_with_a_tenant_cap_counts_both_workflow_handler_keys_together()

Section titled “ Lease_with_a_tenant_cap_counts_both_workflow_handler_keys_together()”
[Fact]
public Task Lease_with_a_tenant_cap_counts_both_workflow_handler_keys_together()

Task

Lease_with_a_tenant_cap_does_not_count_a_job_of_another_handler_key()

Section titled “ Lease_with_a_tenant_cap_does_not_count_a_job_of_another_handler_key()”
[Fact]
public Task Lease_with_a_tenant_cap_does_not_count_a_job_of_another_handler_key()

Task

Lease_with_a_tenant_cap_does_not_count_a_lease_that_has_expired()

Section titled “ Lease_with_a_tenant_cap_does_not_count_a_lease_that_has_expired()”
[Fact]
public Task Lease_with_a_tenant_cap_does_not_count_a_lease_that_has_expired()

Task

Lease_with_a_tenant_cap_does_not_count_the_leases_of_another_tenant()

Section titled “ Lease_with_a_tenant_cap_does_not_count_the_leases_of_another_tenant()”
[Fact]
public Task Lease_with_a_tenant_cap_does_not_count_the_leases_of_another_tenant()

Task

Lease_with_a_tenant_cap_leases_up_to_the_cap_for_one_tenant()

Section titled “ Lease_with_a_tenant_cap_leases_up_to_the_cap_for_one_tenant()”
[Fact]
public Task Lease_with_a_tenant_cap_leases_up_to_the_cap_for_one_tenant()

Task

Lease_with_a_tenant_cap_skips_a_tenant_at_the_cap_and_leases_the_oldest_job_of_another()

Section titled “ Lease_with_a_tenant_cap_skips_a_tenant_at_the_cap_and_leases_the_oldest_job_of_another()”
[Fact]
public Task Lease_with_a_tenant_cap_skips_a_tenant_at_the_cap_and_leases_the_oldest_job_of_another()

Task

Lease_with_a_tenant_cap_still_honors_the_lane_filter()

Section titled “ Lease_with_a_tenant_cap_still_honors_the_lane_filter()”
[Fact]
public Task Lease_with_a_tenant_cap_still_honors_the_lane_filter()

Task

Lease_with_a_tenant_cap_still_leases_a_job_of_another_handler_key_of_a_tenant_at_the_cap()

Section titled “ Lease_with_a_tenant_cap_still_leases_a_job_of_another_handler_key_of_a_tenant_at_the_cap()”
[Fact]
public Task Lease_with_a_tenant_cap_still_leases_a_job_of_another_handler_key_of_a_tenant_at_the_cap()

Task

Lease_with_a_tenant_cap_that_names_no_handler_key_or_no_room_is_refused()

Section titled “ Lease_with_a_tenant_cap_that_names_no_handler_key_or_no_room_is_refused()”
[Fact]
public Task Lease_with_a_tenant_cap_that_names_no_handler_key_or_no_room_is_refused()

Task

Lease_with_an_empty_lanes_list_applies_no_filter()

Section titled “ Lease_with_an_empty_lanes_list_applies_no_filter()”
[Fact]
public Task Lease_with_an_empty_lanes_list_applies_no_filter()

Task

Lease_with_null_lanes_applies_no_filter()

Section titled “ Lease_with_null_lanes_applies_no_filter()”
[Fact]
public Task Lease_with_null_lanes_applies_no_filter()

Task

Lease_without_a_tenant_cap_gives_one_tenant_the_whole_queue_in_age_order()

Section titled “ Lease_without_a_tenant_cap_gives_one_tenant_the_whole_queue_in_age_order()”
[Fact]
public Task Lease_without_a_tenant_cap_gives_one_tenant_the_whole_queue_in_age_order()

Task

Leased_job_is_not_given_to_another_worker_before_expiry()

Section titled “ Leased_job_is_not_given_to_another_worker_before_expiry()”
[Fact]
public Task Leased_job_is_not_given_to_another_worker_before_expiry()

Task

Leasing_marks_Leased_and_increments_the_attempt()

Section titled “ Leasing_marks_Leased_and_increments_the_attempt()”
[Fact]
public Task Leasing_marks_Leased_and_increments_the_attempt()

Task

MarkRunningAsync_works_only_for_the_real_owner()

Section titled “ MarkRunningAsync_works_only_for_the_real_owner()”
[Fact]
public Task MarkRunningAsync_works_only_for_the_real_owner()

Task

[Fact]
public Task QueryAsync_filters_by_handler_key()

Task

[Fact]
public Task QueryAsync_filters_by_lane()

Task

[Fact]
public Task QueryAsync_filters_by_tenant()

Task

QueryAsync_lane_filter_does_not_leak_another_tenants_job()

Section titled “ QueryAsync_lane_filter_does_not_leak_another_tenants_job()”
[Fact]
public Task QueryAsync_lane_filter_does_not_leak_another_tenants_job()

Task

QueryAsync_returns_nothing_for_a_handler_key_no_job_uses()

Section titled “ QueryAsync_returns_nothing_for_a_handler_key_no_job_uses()”
[Fact]
public Task QueryAsync_returns_nothing_for_a_handler_key_no_job_uses()

Task

QueryAsync_treats_an_empty_handler_key_as_a_filter_that_matches_nothing()

Section titled “ QueryAsync_treats_an_empty_handler_key_as_a_filter_that_matches_nothing()”
[Fact]
public Task QueryAsync_treats_an_empty_handler_key_as_a_filter_that_matches_nothing()

Task

Queue_depth_counts_a_job_released_for_retry_as_pending_again()

Section titled “ Queue_depth_counts_a_job_released_for_retry_as_pending_again()”
[Fact]
public Task Queue_depth_counts_a_job_released_for_retry_as_pending_again()

Task

Queue_depth_counts_every_tenants_jobs_together()

Section titled “ Queue_depth_counts_every_tenants_jobs_together()”
[Fact]
public Task Queue_depth_counts_every_tenants_jobs_together()

Task

Queue_depth_drops_a_lane_once_its_last_job_finishes()

Section titled “ Queue_depth_drops_a_lane_once_its_last_job_finishes()”
[Fact]
public Task Queue_depth_drops_a_lane_once_its_last_job_finishes()

Task

Queue_depth_groups_pending_jobs_by_lane()

Section titled “ Queue_depth_groups_pending_jobs_by_lane()”
[Fact]
public Task Queue_depth_groups_pending_jobs_by_lane()

Task

Queue_depth_is_empty_when_no_job_is_open()

Section titled “ Queue_depth_is_empty_when_no_job_is_open()”
[Fact]
public Task Queue_depth_is_empty_when_no_job_is_open()

Task

Queue_depth_never_counts_a_terminal_status()

Section titled “ Queue_depth_never_counts_a_terminal_status()”
[Fact]
public Task Queue_depth_never_counts_a_terminal_status()

Task

Queue_depth_separates_the_open_statuses()

Section titled “ Queue_depth_separates_the_open_statuses()”
[Fact]
public Task Queue_depth_separates_the_open_statuses()

Task

ReleaseForRetryAsync_returns_to_pending_without_resetting_the_attempt()

Section titled “ ReleaseForRetryAsync_returns_to_pending_without_resetting_the_attempt()”
[Fact]
public Task ReleaseForRetryAsync_returns_to_pending_without_resetting_the_attempt()

Task

ReportItemAsync_counts_successful_and_failed_items_separately()

Section titled “ ReportItemAsync_counts_successful_and_failed_items_separately()”
[Fact]
public Task ReportItemAsync_counts_successful_and_failed_items_separately()

Task

[Fact]
public Task ReportItemAsync_is_idempotent()

Task

[Fact]
public Task Retry_preserves_the_jobs_lane()

Task

SeedAsync(string, string, CancellationToken)

Section titled “ SeedAsync(string, string, CancellationToken)”

Writes a sample record for the given tenant.

protected override ValueTask<object> SeedAsync(string tenantId, string name, CancellationToken cancellationToken)

tenantId string

The tenant that owns the record.

name string

The name distinguishing the record. The same name may be used by two tenants.

cancellationToken CancellationToken

The token every store call inside the hook must be handed. The cancellation contract calls this hook with a token that is already cancelled, so a hook that drops the token reports a store as sound when it is not.

ValueTask<object>

The key to use for reading the record back.

Attempts to delete the record on behalf of the given tenant.

protected override ValueTask<bool?> TryDeleteAsync(string tenantId, object key)

tenantId string

The tenant attempting the delete.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool?>

true if the delete succeeded, false if the record was not found; null if the store does not offer deletion.

There is no delete in the job queue; the only destructive operation available to a tenant is cancel.