I am trying to observe my workers but they are always in queued state or sometime it's RUNNING but never SUCCEED or FAILED.
is workStatus.state from return in doWork() or it's different?
this is my worker script:
package com.mockie.daikokuten.sync.workers
import androidx.work.Worker
class TestWorker:Worker()
{
override fun doWork():Worker.Result
{
return Worker.Result.SUCCESS
}
}
this is script to observe the workers :
val test = PeriodicWorkRequest.Builder(
TestWorker::class.java,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS)
.addTag("test_worker")
.build()
WorkManager.getInstance()?.enqueueUniquePeriodicWork("test_worker", ExistingPeriodicWorkPolicy.KEEP, test)
WorkManager.getInstance()!!.getStatusesByTag("test_worker")
.observe(this, Observer { workStatus ->
if (workStatus != null)
{
for(ws in workStatus)
{
Log.d(":dump2 id ", ws.id.toString())
Log.d(":dump2 tag", ws.tags.toString())
Log.d(":dump2 state", ws.state.toString())
}
}
})
this is the result in Logcat:
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 id: 5c6297f7-11d8-4f2f-a327-773672a7435c
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 tag: [test_worker, com.mockie.daikokuten.sync.workers.TestWorker]
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 state: ENQUEUED
For your periodic work request you should see
ENQUEUED - RUNNING - ENQUEUED
where the latter ENQUEUED is the state of the next work request.
You might get very briefly a SUCCEEDED between RUNNING and ENQUEUED, but I have never seen that.
For a onetime work request you see
ENQUEUED - RUNNING - SUCCEEDED
or whatever you return in doWork().
(Android 8.1 API 27, 1.0.0-alpha04)
This is for anyone who is having trouble getting their output data from periodic work.
It's more like a hack.
In your Worker, just define a static mutable Live Data.
At the place where you observe your work's state, observe this live data when your state turns to "RUNNING".
Here's a template :
The actual Worker:
public class SomeWorker extends Worker{
//This live data can be of any type. I'm setting Boolean
Public static MutableLiveData<Boolean> outputObservable = new MutableLiveData();
private boolean output_boolean;
try{
//Do you work here post your result to the live data
output_boolean = SomeTaskThatReturnsABoolean();
outputObservable.postValue(output_boolean);
return Result.Success();
}catch(Exception e){
e.printStackTrace();
outputObservable.postValue(!output_boolean);
return Result.Failure();
}
}
Your activity that observes this worker's info:
//In YourActivity class inside OnCreate
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).observe (this,
new Observer<List<WorkInfo>>(){
#Override
public void onChanged(#Nullable List<WorkInfo> workInfos) {
if(workInfos!=null && (!(workInfos.isEmpty()))) {
WorkInfo info = workInfos.get(0);
switch(info.getState()){
case ENQUEUED:
break;
case RUNNING:
SomeWorker.outputObservable.observe(YourActivity.this,
new Observer<Boolean>(){
#Override
public void onChanged(#Nullable Boolean aBoolean) {
//EDIT: Remove the observer of the worker otherwise
//before execution of your below code, the observation might switch
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).removeObservers(YourActivity.this);
if(aBoolean)
//Do whatever you have to if it's true
else
//Do whatever you have to if it's false
}
}
);
}
}
}
}
);
In this way you can observe your results when the state of the work is under running, before it gets switched back to enqueued.
The above answer is correct. For PeriodicWork you should see:
ENQUEUED -> RUNNING -> ENQUEUED
However, there is a bug in alpha04 which causes PeriodicWork to not run on API >= 23. This will be fixed in alpha05. For more info take a look at https://issuetracker.google.com/issues/111195153.
IMPORTANT: As of a couple of days ago: alpha05 has shipped. This bug is fixed.
Related
I use Android WorkManager to download file from Firebase Storage.
The code is as the following
#NonNull
#Override
public Result doWork() {
dowloadFile-1
dowloadFile-2
dowloadFile-3
dowloadFile-4
return Result.success();
}
// Create and start an unique work.
// It mean there is only one work running at the time
public static void start(Context context) {
Constraints constraints = new Constraints.Builder()
// The Worker needs Network connectivity
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
int backoffDelay = context.getResources().getInteger(R.integer.sync_retry_backoff_delay);
OneTimeWorkRequest worker = new OneTimeWorkRequest.Builder(FirebaseSyncService.class)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, backoffDelay, TimeUnit.MINUTES)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork(SYNC_WORK_ID, ExistingWorkPolicy.KEEP, worker);
}
I use getWorkInfosForUniqueWorkLiveData() to check state of unique work
And the scenario are as the following
Step-1. Connect internet then start work
Step-2. The program completed download file-1 and file-2
The work state now is RUNNING
Step-3. Disconnect internet
Work will be stopped and WorkManager will retry by adding new Work to queue
The work state now is ENQUEUED
Step-4. Re-connect internet
Expected:
After reconnecting internet, I expect the WorkManager will perform new work from beginning and the state is RUNING again.
I want to download file-1 and file-2 again.
Actual
WorkManager continue previous work and resume from downloading file-3. But the state is ENQUEUED.
Even if I change ExistingWorkPolicy.KEEP -> ExistingWorkPolicy.REPLACE, the behavior is the same.
Thank you for all supports
check these in your worker() Java file.
For doing network call you maybe using try catch, if not you have to use and then check in catch block you have to add return Result.Failure. Otherwise, even if you disconnect your internet it will continuous working(doWork() function),even work state is ENQUEUED and you will see exception in log.(Not sure reason, seems to be bug...)
I'm not faced these issue, when i use with Kotlin CoroutineWorker. its happening only if i use Worker
Note : I'll not suggest this type of coding, use Kotlin CoroutineWorker and forLoop for multiple series of file downloads. here i'm showing only for Demo
purpose.
#NonNull
#Override
public Result doWork() {
try {
dowloadFile-1
}catch (Exception e){
return Result.Failure; // this is important.Otherwise worker not finish,even work state is ENQUEUED
}
try {
dowloadFile-2
}catch (Exception e){
return Result.Failure;
}
try {
dowloadFile-3
}catch (Exception e){
return Result.Failure;
}
try {
dowloadFile-4
}catch (Exception e){
return Result.Failure;
}
//all success
return Result.success();
}
Quick Tip, In your case - Its better to use ExistingWorkPolicy.REPLACE, and Network call in background thread(use CoroutineWorker())
I'm using Parse Server for my Android app and everything is working fine, but every time I call saveEventually on a new or old ParseObject, it is taking a really long time. Sometimes it's more than 1 minute for 1 item to return the callback.
Anyone had this problem?
Example:
orderObject.p.apply {
put(ORDER_STATE, ORDER_STATE_FINISHED)
put(ORDER_NEXT_DATE, orderEndDate)
}
createLog("FinishOrderSeq", "OrderActivity - saveOrder - before saveEvent")
orderObject.p.saveEventuallyEx(isOnline(this)){ e ->
createLog("FinishOrderSeq", "OrderActivity - saveOrder - after saveEvent")
if (e == null){
createToast(getString(R.string.order_dialog_success), this)
createOrderCopy(orderObject, dialog)
} else {
createToast(getString(R.string.order_dialog_err), this)
changeButtonState(posBtn, true)
changeButtonState(negBtn, true)
}
}
fun ParseObject.saveEventuallyEx(isOnline: Boolean, callback: (ParseException?) -> Unit){
if (isOnline){
saveEventually{ err ->
callback(err)
}
} else {
saveEventually()
callback(null)
}
}
Also logs as I replaced it with saveInBackground with callback(still 30 seconds):
2020-05-28 14:53:49.805 18673-18673/? I/FinishOrderSeq: OrderActivity - saveOrder - before saveEvent
2020-05-28 14:54:15.694 18673-18673/? I/FinishOrderSeq: OrderActivity - saveOrder - after saveEvent
UPDATE:
So I figured out from parse dashboard, that ParseObject is saved as record in table immediatelly, but callback from saveEventually is sent after 30sec - 2 minutes.
UPDATE 2:
I also tried to use saveInBackground() if user is online (with callback). This also took 30seconds to 2 minutes for callback to return. Object was saved to parse database with all data after 100ms (checked from Parse Dashboard).
Then I thought something is wrong with ParseSDK threads, so I used save() inside Coroutine. Same problem occured here, save() took up to 2 minutes to perform.
Code with coroutine:
fun ParseObject.saveAsync(context: CoroutineContext, scope: CoroutineScope, isOnline: Boolean, callback: (ParseException?) -> Unit){
if (isOnline){
scope.launch {
var ex: ParseException? = null
try {
save()
} catch (e: ParseException){
ex = e
}
withContext(context){
callback(ex)
}
}
}
}
There is some serious problem with callbacks in ParseSDK for Android and I don't know what can cause this. No exception no error on server side.
UPDATE 3:
After deeper investigation, I found which function is taking long time to proceed.
ParseObject.State result = saveTask.getResult();
Approximately 30 seconds - 2 minutes to get into next line of code.
This is lowest level of function I can get inside SDK.
Inside function save() or saveInBackground() there is this inner function in Java:
Task<Void> saveAsync(final String sessionToken, final Task<Void> toAwait) {
if (!isDirty()) {
return Task.forResult(null);
}
final ParseOperationSet operations;
synchronized (mutex) {
updateBeforeSave();
validateSave();
operations = startSave();
}
Task<Void> task;
synchronized (mutex) {
// Recursively save children
/*
* TODO(klimt): Why is this estimatedData and not... I mean, what if a child is
* removed after save is called, but before the unresolved user gets resolved? It
* won't get saved.
*/
task = deepSaveAsync(estimatedData, sessionToken);
}
return task.onSuccessTask(
TaskQueue.<Void>waitFor(toAwait)
).onSuccessTask(new Continuation<Void, Task<ParseObject.State>>() {
#Override
public Task<ParseObject.State> then(Task<Void> task) {
final Map<String, ParseObject> fetchedObjects = collectFetchedObjects();
ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects);
return getObjectController().saveAsync(getState(), operations, sessionToken, decoder);
}
}).continueWithTask(new Continuation<ParseObject.State, Task<Void>>() {
#Override
public Task<Void> then(final Task<ParseObject.State> saveTask) {
ParseObject.State result = saveTask.getResult(); <--- THIS IS TAKING LONG TIME
return handleSaveResultAsync(result, operations).continueWithTask(new Continuation<Void, Task<Void>>() {
#Override
public Task<Void> then(Task<Void> task) {
if (task.isFaulted() || task.isCancelled()) {
return task;
}
// We still want to propagate saveTask errors
return saveTask.makeVoid();
}
});
}
});
}
From the docs:
Most save functions execute immediately, and inform your app when the save is complete. If you don’t need to know when the save has finished, you can use saveEventually instead.
It can take a long time because with saveEventually you are basically saying "save it soon". If you want to "save it as soon a possible" then use saveInBackground as described in the docs.
Further it says:
All calls to saveEventually (and deleteEventually) are executed in the order they are called, so it is safe to call saveEventually on an object multiple times. If you have the local datastore enabled, then any object you saveEventually will be pinned as long as that save is in progress. That makes it easy to retrieve your local changes while waiting for the network to be available.
Which means that you can save and modify the object locally multiple times and the latest version will be stored in the database as soon as the network connection is reestablished.
We have an Android app using WorkManager to handle background sync work. Our sync worker is like this:
public class SyncWorker extends Worker {
[...]
#NonNull
#Override
public Result doWork() {
if (canNotRetry(getRunAttemptCount())) {
// This could seem unreachable, consider removing... or not... because if stopped by the
// system, the work might be retried by design
CBlogger.INSTANCE.log([...]);
return Result.success();
}
boolean syncOk = false;
//Sync
try (Realm realm = Realm.getDefaultInstance()) {
// Doing sync related ops & network calls
// checking this.isStopped() between operations to quit
// sync activity when worker has to be stopped
syncOk = true;
} catch (Throwable throwable) {
CBlogger.INSTANCE.log([...]);
}
// On error, continue with following code to avoid any logic in catch
// This method must NOT throw any unhandled exception to avoid unique work to be marked as failed
try {
if (syncOk) {
return Result.success();
}
if (canNotRetry(getRunAttemptCount() + 1)) {
CBlogger.INSTANCE.log([...]);
return Result.success();
} else {
CBlogger.INSTANCE.log([...]);
return Result.retry();
}
} catch (Throwable e) {
CBlogger.INSTANCE.log([...]);
return Result.success();
}
}
private boolean canNotRetry(int tryNumber) {
// Check if the work has been retry too many times
if (tryNumber > MAX_SYNC_RETRY_COUNT) {
CBlogger.INSTANCE.log([...]);
return true;
} else {
return false;
}
}
#Override
public void onStopped() {
CBlogger.INSTANCE.log([...]);
}
}
The work is scheduled by a dedicate method of an helper class:
public static void scheduleWorker(Context context, String syncPolicy, ExistingWorkPolicy existingWorkingPolicy){
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(false)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
Data.Builder data = new Data.Builder();
data.putString(context.getResources().getString(R.string.sync_worker_policy), syncPolicy);
Log.d(TAG, "Scheduling one-time sync request");
logger.info("Scheduling one-time sync request");
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder
(SyncWorker.class)
.setInputData(data.build())
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniqueWork("OneTimeSyncWorker", existingWorkingPolicy, oneTimeWorkRequest);
}
that is called when user clicks on "Sync" button or by another worker that is scheduled to run every 20' and calls the helper's function this way:
SyncWorkerManager.scheduleWorker(context, context.getResources().getString(R.string.sync_worker_policy_full), ExistingWorkPolicy.KEEP);
so that a new sync is queued only if not already waiting or running. Notice that sync work policy enforces that a connected network is required.
This strategy works all in all good, but sometimes we find in logs that Worker's onStopped() method is called a few seconds (about 10") after SyncWorker start.
Known that we never programmatically stop a specific Worker for the outside and we only call WorkManager.getInstance(context).cancelAllWork(); during logout procedure or before a new login (that also schedules del periodic Worker), when does the system can decide to stop the worker and call its onStopped() method?
I know that it can happen when:
Constraints are no longer satisfied (network connection dropped)
Worker runs over 10' limit imposed by JobScheduler implementation (our scenario is tested on Android 9 device)
New unique work enqueued with same name and REPLACE policy (we never use this policy in our app for the SyncWorker, only for PeriodicSyncWorker)
Spurious calls due to this bug (we work with "androidx.work:work-runtime:2.2.0")
Is there any other condition that can cause Worker's to be stopped? Something like:
Doze mode
App stand-by buckets
App background restrictions (Settings --> Apps --> My App --> Battery --> Allow Background)
App battery optimization (Settings --> Apps --> My App --> Battery --> Battery Optimization)
Thanks
There are multiple reasons a Worker can be stopped. You can explicitly ask for it to be cancelled or WorkManager might stop it for a variety of reasons which are documented here.
We are using OneTimeWorkRequest to start background task in our project.
At application start, we are starting the OneTimeWorkRequest (say req A)
Depends on user's action we start the same work request A.
At some cases, if the app gets killed when the work request A is in progress, Android automatically restarts the request A when the app restarts. Once again we are also starting the request A again. So two instances of the request A runs in parallel and leads to a deadlock.
To avoid this, I did below code in app start to check if the worker is running but this always returns false.
public static boolean isMyWorkerRunning(String tag) {
List<WorkStatus> status = WorkManager.getInstance().getStatusesByTag(tag).getValue();
return status != null;
}
Is there a better way to handle this?
I checked the beginUniqueWork(). Is it costlier if I have only one request?
Edit 2:
This question is about unique One time task. For starting unique Periodic task we had a separate API enqueueUniquePeriodicWork(). But we did not have an API for starting unique onetime work. I was confused to use between continuation object or manually check and start approach.
In recent build they Android added new api for this enqueueUniqueWork(). This is the exact reason they mentioned in their release notes.
Add WorkManager.enqueueUniqueWork() API to enqueue unique
OneTimeWorkRequests without having to create a WorkContinuation.
https://developer.android.com/jetpack/docs/release-notes
Edit 2:
Nov 8th release notes:
https://developer.android.com/jetpack/docs/release-notes
Add WorkManager.enqueueUniqueWork() API to enqueue unique
OneTimeWorkRequests without having to create a WorkContinuation.
This says, alpha11 has this new API to uniquely enqueue a onetimework.
I tried changing the code as follows:
OneTimeWorkRequest impWork = new OneTimeWorkRequest.Builder(WorkerNotesAttachment.class)
.addTag(RWORK_TAG_NOTES)
.build();
WorkManager.getInstance().enqueueUniqueWork(RWORK_TAG_NOTES, ExistingWorkPolicy.REPLACE, impWork);
I tried using the beginUniqueWork API. But it fails to run sometimes. So I ended up writing the following function.
public static boolean isMyWorkerRunning(String tag) {
List<WorkStatus> status = null;
try {
status = WorkManager.getInstance().getStatusesByTag(tag).get();
boolean running = false;
for (WorkStatus workStatus : status) {
if (workStatus.getState() == State.RUNNING
|| workStatus.getState() == State.ENQUEUED) {
return true;
}
}
return false;
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return false;
}
We need to get all the WorkStatus objects and check if atleast one of them is in running or Enqueued state. As the system keeps all the completed works in the DB for few days (Refer pruneWork()), we need to check all the work instances.
Invoke this function before starting the OneTimeWorkRequest.
public static void startCacheWorker() {
String tag = RWORK_TAG_CACHE;
if (isMyWorkerRunning(tag)) {
log("worker", "RWORK: tag already scheduled, skipping " + tag);
return;
}
// Import contact for given network
OneTimeWorkRequest impWork = new OneTimeWorkRequest.Builder(WorkerCache.class)
.addTag(tag)
.build();
WorkManager.getInstance().enqueue(impWork);
}
You can use beginUniqueWork() with a unique name.
If you use ExistingWorkPolicy:
APPEND: the 2 requests will run serial.
KEEP: will not run the second request if the first is running.
REPLACE: the 2 requests will run parallel.
Using getStatusesByTag returns LiveData of List<WorkStatus>
it was made as LiveData because WorkStatus is kept in Room DB and WorkManger has to query it first on background thread then deliver the result.
so you must observe to get the real value when it's available .
calling getValue() will return last value of the LiveData which isn't available on the time you call it.
What you can do
public static LiveData<Boolean> isMyWorkerRunning(String tag) {
MediatorLiveData<Boolean> result = new MediatorLiveData<>();
LiveData<List<WorkStatus>> statusesByTag = WorkManager.getInstance().getStatusesByTag(tag);
result.addSource(statusesByTag, (workStatuses) -> {
boolean isWorking;
if (workStatuses == null || workStatuses.isEmpty())
isWorking = false;
else {
State workState = workStatuses.get(0).getState();
isWorking = !workState.isFinished();
}
result.setValue(isWorking);
//remove source so you don't get further updates of the status
result.removeSource(statusesByTag);
});
return result;
}
Now you don't start the task until you observe on the returning value of isMyWorkerRunning if it's true then it's safe to start it if not this mean that another task with the same tag is running
Since all of the answers are mostly outdated, you can listen for changes on a tagged worker like this:
LiveData<List<WorkInfo>> workInfosByTag = WorkManager.getInstance().getWorkInfosByTagLiveData(tag);
workInfosByTag.observeForever(workInfos -> {
for (WorkInfo workInfo : workInfos) {
workInfo.toString();
}
});
I got the following code using Mosby.
Fragment:
#Override
public Observable<CardInfo> loadCardIntent() {
return Observable.just(new CardInfo(cardselected, PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(PreferencesVariables.SHOW_BACK_CARD.toString(), false)))
//.delay(500, TimeUnit.MILLISECONDS)
.doOnNext(showBack -> Log.d(TAG, "Show card back: " + showBack));
}
#Override
public Observable<CardInfo> loadFrontIntent() {
return RxView.clicks(cardBackImageView)
.map(showFront -> new CardInfo(cardselected, false))
.doOnNext(showFront -> Log.d(TAG, "Show card front"));
}
#Override
public Observable<Boolean> hideCardIntent() {
return clicks(cardFrontImageView)
.map(ignored -> true)
.doOnNext(close -> Log.d(TAG, "Close card activity"));
}
Presenter:
#Override
protected void bindIntents() {
Observable<CardViewState> showSelectedCard = intent(CardView::loadCardIntent)
.switchMap(cardInfo -> interactor.getCard(cardInfo))
.doOnError(error -> System.out.print(error.getMessage()));
Observable<CardViewState> showFront = intent(CardView::loadFrontIntent)
.switchMap(cardInfo -> interactor.getCard(cardInfo))
.doOnError(error -> System.out.print(error.getMessage()));
Observable<CardViewState> hideCard = intent(CardView::hideCardIntent)
.switchMap(ignored -> interactor.hideCard());
Observable<CardViewState> intents = Observable.merge(showSelectedCard, showFront, hideCard)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
subscribeViewState(intents, CardView::render);
}
Fragment:
#Override
public void render(CardViewState viewState) {
if (viewState instanceof CardViewState.CardBackState) {
renderCard(R.raw.planningpoker_rueckseite, cardBackImageView);
renderCard(((CardViewState.CardBackState) viewState).card, cardFrontImageView);
showCardBack();
} else if (viewState instanceof CardViewState.CardFrontState) {
renderCard(R.raw.planningpoker_rueckseite, cardBackImageView);
renderCard(((CardViewState.CardFrontState) viewState).card, cardFrontImageView);
showCardFront();
} else if (viewState instanceof CardViewState.CardCloseState) {
getActivity().finish();
}
}
Interactor:
Observable<CardViewState> getCard(CardInfo cardInfo) {
return cardInfo.showBack ? Observable.just(new CardViewState.CardBackState(CARDS[cardInfo.card])) :
Observable.just(new CardViewState.CardFrontState(CARDS[cardInfo.card]));
}
Observable<CardViewState> hideCard() {
return Observable.just(new CardViewState.CardCloseState());
}
Without the delay in loadCardIntent() the render()-method does not get triggered with the CardBackState. But I don't want to use a arbitrary delay to ensure the right methods get triggered.
Is there any other way to ensure that all events get emitted?
Thanks for the help.
Hm, is your code available on github somewhere? So far everything seems to be ok. Maybe it is an internal mosby bug. Is it working if you add subscribeOn(schdulers.io()) to loadCardIntent() in presenters bind() method.
The only difference I see with or without delay() is that your code runs sync (on main UI thread) whereas delay() switches the execution of your code to a background thread. Are you sure your interactor.getCardInfo() is meant to run on androids main UI thread? I.e. if it runs on main thread, but you are doing a http request (on main UI thread) an exception is thrown. Do you catch exceptions in interactor?
This was a mosby internal issue and has been fixed now.
See https://github.com/sockeqwe/mosby/issues/242
Please use latest snapshot:
com.hannesdorfmann.mosby3:mvi:3.0.4-SNAPSHOT
(see README) to verify everything works now as intended.
Please comment on the linked github issue if it fixes your problem or not.
Thanks.
My solution for now is to use Schedulers.trampoline(). It is not ideal and in no way sufficient but it allows me to get rid of the delay that is more of a hassle.
The problem that Schedulers.trampoline() seem to be solving is that the change onto another thread takes a short amount of time. And that causes the event to get lost. So staying on the same thread fixes this.