Skip to content
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

Small tweaks and cleanup #196

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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 @@ -9,11 +9,15 @@
import java.io.File;

/**
* Ideally this would be called `PythonWorkerImpl` but the name is used in the native code.
* Worker implementation that executes Python code.
*
* Ideally this would be called `PythonWorkerImpl` but the name is used in the native
* python-for-android code.
*/
public class PythonWorker {
private static final String TAG = "PythonWorkerImpl";
// Python environment variables
private final Context context;
private final String pythonName;
private final String workerEntrypoint;
private final String androidPrivate;
Expand All @@ -22,7 +26,7 @@ public class PythonWorker {
private final String pythonPath;

public PythonWorker(@NonNull Context context, String pythonName, String workerEntrypoint) {
PythonLoader.doLoad(context);
this.context = context;
this.pythonName = pythonName;
this.workerEntrypoint = workerEntrypoint;

Expand All @@ -33,6 +37,15 @@ public PythonWorker(@NonNull Context context, String pythonName, String workerEn
pythonPath = appRoot + ":" + appRoot + "/lib";
}

/**
* Prepare the Python environment.
*
* This should be called before any calls to `execute`.
*/
public void prepare() {
PythonLoader.doLoad(context);
}

// Native part
public static native int nativeStart(
String androidPrivate, String androidArgument,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.learningequality;

import android.app.ActivityManager;
import android.content.Context;
import android.os.Process;

import org.kivy.android.PythonActivity;
import org.learningequality.Kolibri.WorkerService;
Expand All @@ -23,4 +25,21 @@ public static boolean isActivityContext() {
public static boolean isServiceContext() {
return WorkerService.mService != null;
}

/**
* Get the name of the current process.
*
* @param context - the context to use
* @return the name of the current process as a string, or an empty string if not found
*/
public static String getCurrentProcessName(Context context) {
int pid = Process.myPid();
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
return "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import androidx.work.Configuration;

import org.kivy.android.PythonContext;
import org.learningequality.ContextUtil;
import org.learningequality.notification.NotificationRef;

import java.util.concurrent.Executors;
Expand All @@ -27,8 +28,12 @@ public void onCreate() {
// Initialize Python context
PythonContext.getInstance(this);
createNotificationChannels();
// Register activity lifecycle callbacks
registerActivityLifecycleCallbacks(new KolibriActivityLifecycleCallbacks());

String currentProcessName = ContextUtil.getCurrentProcessName(this);
if (currentProcessName.endsWith(getString(R.string.task_worker_process))) {
// Register activity lifecycle callbacks
registerActivityLifecycleCallbacks(new KolibriActivityLifecycleCallbacks());
}
WorkController.getInstance(this).wake();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.work.ListenableWorker;
import androidx.work.WorkerParameters;

import org.learningequality.task.Worker;
Expand All @@ -23,6 +24,10 @@ public BackgroundWorker(
) {
super(context, workerParams);
workerImpl = new PythonWorker(context, "TaskWorker", "taskworker.py");

// Ideally we wouldn't call this in the constructor, but we can't override `startWork` to
// call it just before `doWork` is called.
workerImpl.prepare();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public ListenableFuture<Result> startRemoteWork() {
final String id = getId().toString();
final String arg = getArgument();

workerImpl.prepare();

// See executor defined in configuration
final ThreadPoolExecutor executor = (ThreadPoolExecutor) getBackgroundExecutor();
// This is somewhat similar to what the plain `Worker` class does, except that we
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,115 +7,78 @@
import androidx.work.OneTimeWorkRequest;
import androidx.work.multiprocess.RemoteWorkManager;

import org.learningequality.ContextUtil;
import org.learningequality.FuturesUtil;
import org.learningequality.Kolibri.R;
import org.learningequality.Kolibri.sqlite.JobStorage;
import org.learningequality.sqlite.query.UpdateQuery;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;

import java9.util.concurrent.CompletableFuture;


public class Reconciler implements AutoCloseable {
public static final String TAG = "Kolibri.TaskReconciler";
public static final String LOCK_FILE = "kolibri_reconciler.lock";
private static final AtomicBoolean lock = new AtomicBoolean(false);

private final RemoteWorkManager workManager;
private final LockChannel lockChannel;
private final JobStorage db;
private final Executor executor;
private FileLock lock;

protected static class LockChannel {
private static LockChannel mInstance;
private final FileChannel channel;

public LockChannel(File lockFile) {
try {
channel = new RandomAccessFile(lockFile, "rw").getChannel();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public FileLock tryLock() {
try {
return this.channel.tryLock();
} catch (IOException e) {
Log.e(TAG, "Failed to acquire lock", e);
return null;
}
}

public static LockChannel getInstance(Context context) {
if (mInstance == null) {
File lockFile = new File(context.getFilesDir(), LOCK_FILE);
mInstance = new LockChannel(lockFile);
}
return mInstance;
}
}


public Reconciler(RemoteWorkManager workManager, JobStorage db, LockChannel lockChannel, Executor executor) {
public Reconciler(RemoteWorkManager workManager, JobStorage db, Executor executor) {
this.workManager = workManager;
this.db = db;
this.lockChannel = lockChannel;
this.executor = executor;

}

/**
* Create a new Reconciler instance from a Context
* @param context The context to use
* @return A new Reconciler instance
*/
public static Reconciler from(Context context, JobStorage db, Executor executor) {
public static Reconciler from(Context context, JobStorage db, Executor executor) throws RuntimeException {
// Ensure that we're in the task worker process
String expectedProcessSuffix = context.getString(R.string.task_worker_process);
String currentProcessName = ContextUtil.getCurrentProcessName(context);
if (!currentProcessName.endsWith(expectedProcessSuffix)) {
throw new RuntimeException("Refusing to create Reconciler in process " + currentProcessName);
}
RemoteWorkManager workManager = RemoteWorkManager.getInstance(context);
return new Reconciler(workManager, db, LockChannel.getInstance(context), executor);
return new Reconciler(workManager, db, executor);
}

/**
* Attempt to acquire an exclusive lock on the lock file, which will prevent multiple
* Reconciler instances from running at the same time, including in different processes.
* Also starts a transaction on the database.
* Synchronizes on the atomic boolean as a locking mechanism, which will prevent multiple
* Reconciler instances from running at the same time. Reconciler.from already prevents this
* from running in multiple processes.
* @return True if the lock was acquired, false otherwise
*/
public boolean begin() {
// First get a lock on the lock file
Log.d(TAG, "Acquiring lock");
lock = lockChannel.tryLock();
if (lock == null) {
Log.d(TAG, "Failed to acquire lock");
return false;
synchronized (lock) {
if (lock.get()) {
Log.d(TAG, "Lock already acquired");
return false;
}
lock.set(true);
}

// Then start a transaction
Log.d(TAG, "Beginning transaction");
// db.begin();
return true;
}

/**
* Commit the database transaction and release the lock
*/
public void end() {
Log.d(TAG, "Committing transaction");
// db.commit();

try {
Log.d(TAG, "Releasing lock");
if (lock != null) lock.release();
} catch (Exception e) {
Log.e(TAG, "Failed to close and release lock", e);
Log.d(TAG, "Releasing lock");
synchronized (lock) {
lock.set(false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ public static CompletableFuture<Boolean> reconcile(Context context, Executor exe

final AtomicBoolean didReconcile = new AtomicBoolean(false);
final JobStorage db = JobStorage.readwrite(context);
final Reconciler reconciler = Reconciler.from(context, db, executor);

final Reconciler reconciler;
try {
reconciler = Reconciler.from(context, db, executor);
} catch (Exception e) {
Log.e(TAG, "Failed to create reconciler", e);
return CompletableFuture.completedFuture(false);
}

if (db == null) {
Log.e(Sentinel.TAG, "Failed to open job storage database");
Expand Down