Skip to content

Jobscheduler #315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Mar 28, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import android.support.annotation.VisibleForTesting;
import com.google.android.datatransport.TransportFactory;
import com.google.android.datatransport.runtime.scheduling.Scheduler;
import com.google.android.datatransport.runtime.scheduling.jobscheduling.Uploader;
import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard;
import com.google.android.datatransport.runtime.time.Clock;
import com.google.android.datatransport.runtime.time.Monotonic;
Expand All @@ -43,19 +44,22 @@ public class TransportRuntime implements TransportInternal {
private final Clock uptimeClock;
private final Scheduler scheduler;
private final SynchronizationGuard guard;
private final Uploader uploader;

@Inject
TransportRuntime(
BackendRegistry backendRegistry,
@WallTime Clock eventClock,
@Monotonic Clock uptimeClock,
Scheduler scheduler,
SynchronizationGuard guard) {
SynchronizationGuard guard,
Uploader uploader) {
this.backendRegistry = backendRegistry;
this.eventClock = eventClock;
this.uptimeClock = uptimeClock;
this.scheduler = scheduler;
this.guard = guard;
this.uploader = uploader;
}

/**
Expand Down Expand Up @@ -98,6 +102,11 @@ public TransportFactory newFactory(String backendName) {
return new TransportFactoryImpl(backendName, this);
}

@RestrictTo(RestrictTo.Scope.LIBRARY)
public Uploader getUploader() {
return uploader;
}

@Override
public void send(SendRequest request) {
scheduler.schedule(request.getBackendName(), convert(request));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@

package com.google.android.datatransport.runtime;

import android.content.Context;
import android.os.Build;
import com.google.android.datatransport.runtime.scheduling.ImmediateScheduler;
import com.google.android.datatransport.runtime.scheduling.Scheduler;
import com.google.android.datatransport.runtime.scheduling.jobscheduling.AlarmManagerScheduler;
import com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoScheduler;
import com.google.android.datatransport.runtime.scheduling.jobscheduling.WorkScheduler;
import com.google.android.datatransport.runtime.scheduling.persistence.EventStore;
import com.google.android.datatransport.runtime.scheduling.persistence.SQLiteEventStore;
import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard;
Expand Down Expand Up @@ -50,7 +55,22 @@ static Clock uptimeClock() {
}

@Provides
static Scheduler scheduler(Executor executor, BackendRegistry registry) {
static WorkScheduler workScheduler(
Context context, EventStore eventStore, @WallTime Clock eventClock) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return new JobInfoScheduler(context, eventStore, eventClock);
} else {
return new AlarmManagerScheduler(context, eventStore, eventClock);
}
}

@Provides
static Scheduler scheduler(
Executor executor,
BackendRegistry registry,
WorkScheduler workScheduler,
EventStore eventStore,
SynchronizationGuard guard) {
return new ImmediateScheduler(executor, registry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,68 @@

package com.google.android.datatransport.runtime.scheduling;

import com.google.android.datatransport.runtime.BackendRegistry;
import com.google.android.datatransport.runtime.EventInternal;
import com.google.android.datatransport.runtime.TransportBackend;
import com.google.android.datatransport.runtime.TransportRuntime;
import com.google.android.datatransport.runtime.scheduling.jobscheduling.WorkScheduler;
import com.google.android.datatransport.runtime.scheduling.persistence.EventStore;
import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;

/**
* Placeholder for the eventual scheduler that will support persistence, retries, respect network
* conditions and QoS.
* Scheduler which persists the events, schedules the services which ultimately logs these events to
* the corresponding backends. This respects network conditions and QoS.
*/
public class DefaultScheduler implements Scheduler {

private static final Logger LOGGER = Logger.getLogger(TransportRuntime.class.getName());
private final WorkScheduler workScheduler;
private final Executor executor;
private final BackendRegistry backendRegistry;
private final EventStore eventStore;
private final SynchronizationGuard guard;
private final int LOCK_TIME_OUT = 10000; // 10 seconds lock timeout

@Inject
public DefaultScheduler(
Executor executor,
BackendRegistry backendRegistry,
WorkScheduler workScheduler,
EventStore eventStore,
SynchronizationGuard guard) {
this.executor = executor;
this.backendRegistry = backendRegistry;
this.workScheduler = workScheduler;
this.eventStore = eventStore;
this.guard = guard;
}

/**
* Schedules the events to be eventually logged to the backend.
*
* @param backendName The backend to which the event needs to be logged.
* @param event The event itself which needs to be logged with additional information.
*/
@Override
public void schedule(String backendName, EventInternal event) {
// TODO: implement.
executor.execute(
() -> {
TransportBackend transportBackend = backendRegistry.get(backendName);
if (transportBackend == null) {
LOGGER.warning(String.format("Logger backend '%s' is not registered", backendName));
return;
}
EventInternal decoratedEvent = transportBackend.decorate(event);
guard.runCriticalSection(
LOCK_TIME_OUT,
() -> {
eventStore.persist(backendName, decoratedEvent);
workScheduler.schedule(backendName, 0);
return null;
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.android.datatransport.runtime.scheduling.jobscheduling;

import android.content.Context;
import com.google.android.datatransport.runtime.scheduling.persistence.EventStore;
import com.google.android.datatransport.runtime.time.Clock;
import javax.inject.Inject;

/**
* Schedules the AlarmManager service based on the backendname. Used for Api levels 20 and below.
*/
public class AlarmManagerScheduler implements WorkScheduler {

private final Context context;

private final EventStore eventStore;

private final Clock clock;

@Inject
public AlarmManagerScheduler(Context applicationContext, EventStore eventStore, Clock clock) {
this.context = applicationContext;
this.eventStore = eventStore;
this.clock = clock;
}

@Override
public void schedule(String backendName, int attemptNumber) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.android.datatransport.runtime.scheduling.jobscheduling;

import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.os.PersistableBundle;
import android.support.annotation.RequiresApi;
import android.support.annotation.VisibleForTesting;
import com.google.android.datatransport.runtime.scheduling.persistence.EventStore;
import com.google.android.datatransport.runtime.time.Clock;
import java.util.zip.Adler32;
import javax.inject.Inject;

/**
* Schedules the service {@link JobInfoSchedulerService} based on the backendname. Used for Apis 21
* and above.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobInfoScheduler implements WorkScheduler {

private final Context context;

private final EventStore eventStore;

private final Clock clock;

private final int DELTA = 30000; // 30 seconds delta

@Inject
public JobInfoScheduler(Context applicationContext, EventStore eventStore, Clock clock) {
this.context = applicationContext;
this.eventStore = eventStore;
this.clock = clock;
}

@VisibleForTesting
int getJobId(String backendName) {
Adler32 checksum = new Adler32();
checksum.update(backendName.getBytes());
return (int) checksum.getValue();
}

private boolean isJobServiceOn(JobScheduler scheduler, int jobId) {
for (JobInfo jobInfo : scheduler.getAllPendingJobs()) {
if (jobInfo.getId() == jobId) {
return true;
}
}
return false;
}

/**
* Schedules the JobScheduler service.
*
* @param backendName The backend to where the events are logged.
* @param attemptNumber Number of times the JobScheduler has tried to log for this backend.
*/
@Override
public void schedule(String backendName, int attemptNumber) {
ComponentName serviceComponent = new ComponentName(context, JobInfoSchedulerService.class);
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
int jobId = getJobId(backendName);
// Check if there exists a job scheduled for this backend name.
if (isJobServiceOn(jobScheduler, jobId)) return;
// Obtain the next available call time for the backend.
long timeDiff = eventStore.getNextCallTime(backendName) - clock.getTime();
// Schedule the build.
PersistableBundle bundle = new PersistableBundle();
bundle.putInt(SchedulerUtil.ATTEMPT_NUMBER, attemptNumber);
bundle.putString(SchedulerUtil.BACKEND_NAME, backendName);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent);
builder.setMinimumLatency(
clock.getTime()
+ SchedulerUtil.getScheduleDelay(timeDiff, DELTA, attemptNumber)); // wait at least
builder.setExtras(bundle);
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
jobScheduler.schedule(builder.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.android.datatransport.runtime.scheduling.jobscheduling;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.Build;
import android.support.annotation.RequiresApi;
import com.google.android.datatransport.runtime.TransportRuntime;

/** The service responsible for uploading information to the backend. */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobInfoSchedulerService extends JobService {

@Override
public boolean onStartJob(JobParameters params) {
String backendName = params.getExtras().getString(SchedulerUtil.BACKEND_NAME);
int attemptNumber = params.getExtras().getInt(SchedulerUtil.ATTEMPT_NUMBER);
TransportRuntime.initialize(getApplicationContext());
TransportRuntime.getInstance()
.getUploader()
.upload(backendName, attemptNumber, () -> this.jobFinished(params, false));
return true;
}

@Override
public boolean onStopJob(JobParameters params) {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.android.datatransport.runtime.scheduling.jobscheduling;

/** Used by the schedulers for some basic constants and utility methods. */
final class SchedulerUtil {

static final String ATTEMPT_NUMBER = "attemptNumber";

static final String BACKEND_NAME = "backendName";

static final String APPLICATION_BUNDLE_ID = "appBundleId";

static final int MAX_ALLOWED_TIME = 100000000;

private SchedulerUtil() {};

static long getScheduleDelay(long backendTimeDiff, int delta, int attemptNumber) {
if (attemptNumber > 11) {
return MAX_ALLOWED_TIME;
}
return Math.max((long) (Math.pow(2, attemptNumber)) * delta, backendTimeDiff);
}
}
Loading