.NET API
JobStoreContract
Tracon.Testing.Contracts.Xunit.dllBehavior 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()
Remarks
Section titled “Remarks”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.
Constructors
Section titled “Constructors”JobStoreContract()
Section titled “ JobStoreContract()”protected JobStoreContract()Methods
Section titled “Methods”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”Remarks
Section titled “Remarks”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()Returns
Section titled “Returns”Remarks
Section titled “Remarks”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”status JobStatus
expected bool
Returns
Section titled “Returns”CompleteAsync_releases_the_lease()
Section titled “ CompleteAsync_releases_the_lease()”[Fact]public Task CompleteAsync_releases_the_lease()Returns
Section titled “Returns”CountAsync(string, CancellationToken)
Section titled “ CountAsync(string, CancellationToken)”Reports how many records the given tenant sees through the listing endpoint.
protected override ValueTask<int> CountAsync(string tenantId, CancellationToken cancellationToken)Parameters
Section titled “Parameters”tenantId string
The reading tenant.
cancellationToken CancellationToken
The token every store call inside the hook must be handed; see TenantIsolationContract.SeedAsync.
Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”ExistsAsync(string, object)
Section titled “ ExistsAsync(string, object)”Reports whether the record is visible from the given tenant’s viewpoint.
protected override ValueTask<bool> ExistsAsync(string tenantId, object key)Parameters
Section titled “Parameters”tenantId string
The reading tenant.
key object
The key returned by TenantIsolationContract.SeedAsync.
Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”Job_not_yet_due_cannot_be_leased()
Section titled “ Job_not_yet_due_cannot_be_leased()”[Fact]public Task Job_not_yet_due_cannot_be_leased()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”QueryAsync_filters_by_handler_key()
Section titled “ QueryAsync_filters_by_handler_key()”[Fact]public Task QueryAsync_filters_by_handler_key()Returns
Section titled “Returns”QueryAsync_filters_by_lane()
Section titled “ QueryAsync_filters_by_lane()”[Fact]public Task QueryAsync_filters_by_lane()Returns
Section titled “Returns”QueryAsync_filters_by_tenant()
Section titled “ QueryAsync_filters_by_tenant()”[Fact]public Task QueryAsync_filters_by_tenant()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”Queue_depth_separates_the_open_statuses()
Section titled “ Queue_depth_separates_the_open_statuses()”[Fact]public Task Queue_depth_separates_the_open_statuses()Returns
Section titled “Returns”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()Returns
Section titled “Returns”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()Returns
Section titled “Returns”ReportItemAsync_is_idempotent()
Section titled “ ReportItemAsync_is_idempotent()”[Fact]public Task ReportItemAsync_is_idempotent()Returns
Section titled “Returns”Retry_preserves_the_jobs_lane()
Section titled “ Retry_preserves_the_jobs_lane()”[Fact]public Task Retry_preserves_the_jobs_lane()Returns
Section titled “Returns”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)Parameters
Section titled “Parameters”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.
Returns
Section titled “Returns”The key to use for reading the record back.
TryDeleteAsync(string, object)
Section titled “ TryDeleteAsync(string, object)”Attempts to delete the record on behalf of the given tenant.
protected override ValueTask<bool?> TryDeleteAsync(string tenantId, object key)Parameters
Section titled “Parameters”tenantId string
The tenant attempting the delete.
key object
The key returned by TenantIsolationContract.SeedAsync.
Returns
Section titled “Returns”true if the delete succeeded, false if the record was not found; null if the store does not offer deletion.
Remarks
Section titled “Remarks”There is no delete in the job queue; the only destructive operation available to a tenant is cancel.