Skip to content

Conversation

@dotMorten
Copy link
Member

/// <summary>
/// Gets the shared job manager.
/// </summary>
public static JobManager Shared { get; } = new JobManager(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this lead to a thread-safety issue? It might be a good idea to use Lazy loading for initialization to ensure safe and efficient singleton creation.

var array = Jobs.Select(j => j.ToJson()).ToArray();
lock (StateLock)
{
SaveState(string.Join("\n", array));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this implementation assumes there won't be any newline characters in actual SaveState data. If any serialized job contains a newline in a field like an error message or log. Splitting on \n character could break the deserialization.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this. Newline-delimited JSON seems really fragile.

}
try
{
// TODO: Check app manifest for background task declaration and provide better instructions how to register the background task
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this requirement be documented in the Readme about silent failures if not configured correctly?

Copy link
Contributor

@prathameshnarkhede prathameshnarkhede Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I was curious if the success and failure behavior are consistent across the platforms?

<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Esri.Calcite.Maui" Version="1.0.0-rc.1" />
</ItemGroup>
Copy link
Contributor

@prathameshnarkhede prathameshnarkhede Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closing tag seems duplicated.

/// <summary>
/// The jobs being managed by the job manager.
/// </summary>
public IList<IJob> Jobs { get; } = new System.Collections.ObjectModel.ObservableCollection<IJob>();
Copy link
Contributor

@prathameshnarkhede prathameshnarkhede Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will reinitialize the Jobs collection every time we call get on Jobs and seems like a bug. We can make use of private backing field which stays for the lifetime of the JobManager.

Comment on lines +147 to +154
public async Task PerformStatusChecks()
{
var tasks = Jobs.Select(async job =>
{
try { await job.CheckStatusAsync(); } catch { }
});
await Task.WhenAll(tasks);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This process could be made faster using SimaphoreSlim or Parallel.ForEachAsync which supports concurrency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not -- CheckStatusAsync yields almost immediately, after signaling a background thread in core. So we queue up all the checks and await them in parallel using Task.WhenAll. This is about as efficient as it can get.

}
}

var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), new Guid().ToString() + ".geodatabase");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Guid() is the default constructor -- it's always going to be 00000000-0000-0000-0000-000000000000. Did you mean to do Guid.NewGuid()?

Suggested change
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), new Guid().ToString() + ".geodatabase");
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Guid.NewGuid().ToString() + ".geodatabase");

private void OnJobPropertyChanged(IJob? oldJob, IJob? newJob)
{
if (oldJob is not null)
oldJob.ProgressChanged -= Job_ProgressChanged;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this unsubscribe from oldJob.StatusChanged too -- like we do for ProgressChanged?

/// </summary>
public void SaveState()
{
var array = Jobs.Select(j => j.ToJson()).ToArray();
Copy link
Collaborator

@mstefarov mstefarov Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events like ProgressChanged/StatusChanged will be coming from background threads and calling SaveState. Do we need some locks around this to avoid overlapping saves? Or e.g. what if user adds a Job (modifying Jobs collection) on the UI thread while SaveState is enumerating in the background? That's a recipe for "collection was modified" exceptions.

Comment on lines +49 to +50
if (value != BackgroundStatusCheckSchedule.Disabled && ValidateInfoPlist())
_preferredBackgroundStatusCheckSchedule = value;
Copy link
Collaborator

@mstefarov mstefarov Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you ever enable this property, it would be impossible to disable due to this if check. Is that intentional?

Comment on lines +201 to +202
return false;
throw new InvalidOperationException($"'BGTaskSchedulerPermittedIdentifiers' must contain '{DefaultsKey}'.");
Copy link
Collaborator

@mstefarov mstefarov Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you undecided about returning or throwing? The xmldoc on PreferredBackgroundStatusCheckSchedule says that we throw, but the setter actually just fails. The throw is unreachable.

Comment on lines +95 to +101
_isBackgroundStatusChecksScheduled = true;
var request = new BGAppRefreshTaskRequest(StatusChecksTaskIdentifier)
{
EarliestBeginDate = NSDate.Now.AddSeconds(PreferredBackgroundStatusCheckSchedule.Interval)
};
NSError error;
BGTaskScheduler.Shared.Submit(request, out error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps check the error before setting _isBackgroundStatusChecksScheduled to true? It might have failed, e.g.

There can be a total of 1 refresh task and 10 processing tasks scheduled at any time. Trying to schedule more tasks returns BGTaskScheduler.Error.Code.tooManyPendingTaskRequests.

Comment on lines +206 to +211
public class BackgroundStatusCheckSchedule
{
public static readonly BackgroundStatusCheckSchedule Disabled = new BackgroundStatusCheckSchedule();
public static BackgroundStatusCheckSchedule RegularInterval(double interval) => new BackgroundStatusCheckSchedule { Interval = interval };
public double Interval { get; private set; }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class could use a bit of doc. Also, it seems that you meant to make this factory-constructible -- should we make the default constructor private?

And while you're at it, the units of interval are not self-explanatory. How about using TimeSpan instead?

private IBackgroundTaskInstance? _taskInstance;

[MTAThread]
public void Run(IBackgroundTaskInstance taskInstance)
Copy link
Collaborator

@mstefarov mstefarov Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the _taskInstance field meant to be initialized here? Nothing assigns to that field right now.

{

var taskId = taskInstance.Task.Name;
manager = JobManager.Create(taskId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the BackgroundTask's JobManager communicate with the original JobManager that started it? Are they meant to pass state around via the shared state file? That seems pretty fragile -- both managers will be trying to read/write that file with no coordination.

As far as I can tell we never name our BackgroundTaskBuilder, so wouldn't taskId always be empty here?

var array = Jobs.Select(j => j.ToJson()).ToArray();
lock (StateLock)
{
SaveState(string.Join("\n", array));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this. Newline-delimited JSON seems really fragile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants