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

Primes Entity cache ahead of time #2928

Draft
wants to merge 6 commits into
base: refactorEntityLoaderTask
Choose a base branch
from

Conversation

shubham1g5
Copy link
Contributor

@shubham1g5 shubham1g5 commented Dec 19, 2024

Summary

https://dimagi.atlassian.net/browse/SC-4055

Spec

Makes 2 improvements for a Cache backed entity list -

  1. Adds a progress indicator to show the loading progress

  2. Uses a Work manager task to schedule entity list cache calculation ahead of time.

Tech Overview

Feature Flag

CACHE_AND_INDEX

PR Checklist

  • If I think the PR is high risk, "High Risk" label is set
  • I have confidence that this PR will not introduce a regression for the reasons below
  • Do we need to enhance manual QA test coverage ? If yes, "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, "Release Note" label is set and a "Release Note" is specified in PR description.

Automated test coverage

Safety story

Copy link

coderabbitai bot commented Dec 19, 2024

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive enhancement to entity loading and caching mechanisms in the CommCare application. The changes span multiple files and focus on improving the user experience during entity selection and loading processes. Key modifications include adding a new progress container in the layout, introducing new string resources for loading states, and implementing a sophisticated entity caching system.

The changes introduce several new classes and interfaces such as PrimeEntityCacheHelper, AndroidAsyncNodeEntityFactory, and EntityLoaderHelper that work together to manage entity loading more efficiently. The implementation allows for background caching of entities, provides progress updates during loading, and ensures that entity preparation can be managed asynchronously.

The modifications also update existing activities and tasks like EntitySelectActivity and EntityLoaderTask to integrate the new caching and loading mechanisms, adding methods for progress reporting and improving the overall robustness of entity loading processes.

Suggested Labels

skip-integration-tests

Suggested Reviewers

  • OrangeAndGreen
  • pm-dimagi
  • avazirna

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (17)
app/src/org/commcare/tasks/EntityLoaderListener.java (1)

17-17: Add documentation for the progress reporting method.

The new method aligns well with the interface's purpose, but would benefit from JavaDoc comments explaining:

  • The meaning of the varargs values (e.g., current progress, total items)
  • Expected value ranges
  • When this method will be called during the loading process
+    /**
+     * Reports progress during entity loading operations
+     * @param values Progress indicators. Typically [current, total] but may include additional metrics
+     */
     void deliverProgress(Integer... values);
app/src/org/commcare/tasks/EntityLoadingProgressListener.java (1)

3-6: Add documentation to clarify usage and thread safety requirements.

The interface would benefit from comprehensive documentation to guide implementers. Consider adding:

  • Interface purpose and usage context
  • Parameter constraints (e.g., valid ranges for progress and total)
  • Thread safety requirements for implementations

Add Javadoc documentation:

 package org.commcare.tasks;

+/**
+ * Listener interface for reporting progress during entity loading operations.
+ * Implementations should be thread-safe as progress updates may come from
+ * background threads.
+ */
 public interface EntityLoadingProgressListener {
+    /**
+     * Reports the current progress of entity loading operation.
+     *
+     * @param progress Current number of entities processed (must be >= 0)
+     * @param total Total number of entities to process (must be >= progress)
+     * @throws IllegalArgumentException if progress < 0 or total < progress
+     */
     void publishEntityLoadingProgress(int progress, int total);
 }
app/src/org/commcare/tasks/EntityLoaderHelper.kt (3)

42-45: Clarify null returns when no entities are loaded
When loadEntities returns null (if no entities are loaded), it could lead to potential NullPointerExceptions in the caller if not carefully handled. Ensure that all call sites properly handle a null result.


49-51: Ensure debug traces are turned off in production
factory.printAndClearTraces("build") might cause unnecessary overhead in production. Consider tying this call to a debug or feature flag.


80-85: Consider early loop termination for performance
Within loadEntitiesWithReferences, you’re iterating through every reference but stopLoading can cause early termination. If you anticipate frequent partial loading, consider a break statement or more explicit check to reduce overhead.

app/src/org/commcare/tasks/EntityLoaderTask.java (3)

21-22: Favor composition or interface over inheritance
EntityLoaderTask extends ManagedAsyncTask. Evaluate if composition or an independent approach might simplify code. This might reduce coupling to ManagedAsyncTask’s life cycle and internal implementation details.


63-65: Consider logging or raising an exception for null results
In onPostExecute, if result is null, the method silently returns. Recommend logging or handling the case more explicitly so that the user or maintainer knows why the result was null.


117-125: Check nullability before using listener
In onProgressUpdate(), you’re calling listener.deliverProgress(values). While it typically won’t be null when onProgressUpdate is triggered, consider a quick safety check to avoid potential NullPointerExceptions.

app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt (1)

28-55: Balance blocking with user experience
prepareEntitiesInternal uses waitForCachePrimeWork if another prime is in progress. This might block user interactions for up to TWO_MINUTES. Explore a non-blocking or more user-friendly strategy (e.g., progress indicator with cancel option or partial data loading).

app/src/org/commcare/tasks/PrimeEntityCacheHelper.kt (3)

25-27: Enforce concurrency constraints
This singleton approach might not be fully thread-safe if multiple threads simultaneously call getInstance(). Kotlin’s double-checked locking is generally safe, but combining it with clearing instance = null (line 151) can reintroduce concurrency concerns.


120-133: Review scheduling logic
primeCacheForDetail attempt to handle caching for either sessionDatum or a list of entities. If neither is provided, the function returns early without caching. Consider logging or clarifying the fallback scenario.


159-162: Check impacts of clearing state in cancel()
cancel() calls clearState(), which sets instance to null. This means a new instance can be created soon after. Ensure that is the intended design, or consider a more persistent approach that retains partial states.

app/src/org/commcare/entity/PrimeEntityCacheListener.kt (2)

3-6: Consider using Kotlin's Pair instead of Android's Pair

The import of android.util.Pair could be replaced with Kotlin's built-in Pair class for better Kotlin interoperability.

-import android.util.Pair
+import kotlin.Pair

7-13: Add KDoc documentation to interface and method

The interface and method would benefit from documentation explaining:

  • The purpose of the interface
  • When callbacks are triggered
  • The meaning of currentDetailInProgress
  • The structure and purpose of cachedEntitiesWithRefs
+/**
+ * Listener interface for entity cache priming completion events.
+ */
 interface PrimeEntityCacheListener {
 
+    /**
+     * Called when entity cache priming completes for a detail
+     * @param currentDetailInProgress The identifier of the detail that was cached
+     * @param cachedEntitiesWithRefs Pair of cached entities and their corresponding references
+     */
     fun onPrimeEntityCacheComplete(
         currentDetailInProgress: String,
         cachedEntitiesWithRefs: Pair<List<Entity<TreeReference>>, List<TreeReference>>
     )
app/src/org/commcare/tasks/PrimeEntityCache.kt (1)

21-23: Consider logging cancellation events

Add logging when the worker is stopped to aid in debugging.

 override fun onStopped() {
+    Logger.debug("PrimeEntityCache worker stopped")
     PrimeEntityCacheHelper.getInstance().cancel()
 }
app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java (1)

107-110: Document why progress updates are ignored

The empty implementation with "nothing to do" comment doesn't explain the rationale. Consider either:

  1. Implementing progress updates for better UX
  2. Adding documentation explaining why progress updates are intentionally ignored
 @Override
 public void deliverProgress(Integer[] values) {
-    // nothing to do
+    // Progress updates are not shown in subnode detail view as they would
+    // interfere with the parent view's progress indication
 }
app/src/org/commcare/tasks/DataPullTask.java (1)

440-440: Consider adding error handling for cache scheduling

While scheduling cache priming after recovery is logical, consider adding error handling to prevent any scheduling failures from affecting the recovery process.

-            PrimeEntityCacheHelper.schedulePrimeEntityCacheWorker();
+            try {
+                PrimeEntityCacheHelper.schedulePrimeEntityCacheWorker();
+            } catch (Exception e) {
+                Logger.log(LogTypes.TYPE_WARNING, 
+                    "Failed to schedule entity cache priming: " + e.getMessage());
+            }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4238469 and d78960d.

📒 Files selected for processing (14)
  • app/res/layout/entity_select_layout.xml (2 hunks)
  • app/res/values/strings.xml (1 hunks)
  • app/src/org/commcare/CommCareApplication.java (3 hunks)
  • app/src/org/commcare/activities/EntitySelectActivity.java (10 hunks)
  • app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt (1 hunks)
  • app/src/org/commcare/entity/PrimeEntityCacheListener.kt (1 hunks)
  • app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java (1 hunks)
  • app/src/org/commcare/tasks/DataPullTask.java (1 hunks)
  • app/src/org/commcare/tasks/EntityLoaderHelper.kt (1 hunks)
  • app/src/org/commcare/tasks/EntityLoaderListener.java (1 hunks)
  • app/src/org/commcare/tasks/EntityLoaderTask.java (3 hunks)
  • app/src/org/commcare/tasks/EntityLoadingProgressListener.java (1 hunks)
  • app/src/org/commcare/tasks/PrimeEntityCache.kt (1 hunks)
  • app/src/org/commcare/tasks/PrimeEntityCacheHelper.kt (1 hunks)
🔇 Additional comments (15)
app/src/org/commcare/tasks/EntityLoaderListener.java (1)

17-17: Verify implementation in all classes implementing EntityLoaderListener.

This interface change requires updates to all implementing classes.

✅ Verification successful

All implementing classes have been verified and are compatible

The search results show that all classes implementing EntityLoaderListener already have the correct deliverProgress method signature:

  • EntitySelectActivity: implements deliverProgress(Integer[] values)
  • EntitySubnodeDetailFragment: implements deliverProgress(Integer[] values)

Both implementations are compatible with the interface method deliverProgress(Integer... values) as varargs and array parameters are interchangeable in Java.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Find all classes that implement EntityLoaderListener
ast-grep --pattern 'class $_ implements $$$EntityLoaderListener$$$'

# Find all classes that extend classes implementing EntityLoaderListener
ast-grep --pattern 'class $_ extends $impl {
  $$$
}' | rg -l 'EntityLoaderListener'

# Search for existing deliverProgress implementations
ast-grep --pattern 'void deliverProgress(Integer... values) {
  $$$
}'

Length of output: 227


Script:

#!/bin/bash
# Let's try a different approach using ripgrep to find implementations
# First, find files containing EntityLoaderListener
rg -l "EntityLoaderListener"

# Then look for deliverProgress implementations with context
rg -A 3 "deliverProgress.*\("

Length of output: 1463

app/src/org/commcare/tasks/EntityLoadingProgressListener.java (1)

5-5: Consider adding parameter validation in implementing classes.

The method parameters should be validated to ensure progress is non-negative and total is greater than or equal to progress.

Let's verify if any implementations already include parameter validation:

Consider creating an abstract base class that handles parameter validation:

public abstract class BaseEntityLoadingProgressListener implements EntityLoadingProgressListener {
    @Override
    public void publishEntityLoadingProgress(int progress, int total) {
        if (progress < 0) {
            throw new IllegalArgumentException("Progress must be non-negative");
        }
        if (total < progress) {
            throw new IllegalArgumentException("Total must be greater than or equal to progress");
        }
        doPublishProgress(progress, total);
    }

    protected abstract void doPublishProgress(int progress, int total);
}
app/src/org/commcare/tasks/EntityLoaderHelper.kt (2)

24-25: Use of mutable factory field can lead to concurrency issues
Since factory is publicly accessible and assigned during initialization, it might be susceptible to concurrency issues if multiple threads can change the factory or call its methods. Consider restricting direct access or using thread-safe patterns to protect state.


97-99: Review cancellation mechanism
stopLoading is set to true in cancel(), but other references to the object might continue execution if they don’t regularly check stopLoading. Make sure that all relevant tasks poll for stopLoading or otherwise handle the canceled state.

app/src/org/commcare/tasks/EntityLoaderTask.java (1)

28-32: Initialize listener consistently
You’re assigning the listener later in attachListener(), but you already have references to it in some places. Ensure the listener is always non-null, or add appropriate guards (e.g., if listener != null) to avoid race conditions.

app/src/org/commcare/entity/AndroidAsyncNodeEntityFactory.kt (1)

81-89: Null checks for detail.id
onPrimeEntityCacheComplete compares detail.id with currentDetailInProgress, but detail.id could be null if detail was not fully initialized. Ensure you handle or log the case where detail.id is null.

app/src/org/commcare/fragments/EntitySubnodeDetailFragment.java (1)

106-110: Verify if progress updates should be propagated

Since this is a subnode detail fragment, verify if progress updates should be propagated to the parent fragment/activity.

✅ Verification successful

Based on the search results, I can now provide a final response:

Empty progress implementation is appropriate for this fragment

The search results show that only EntitySelectActivity meaningfully implements progress updates by showing a loading message with progress counts. Since EntitySubnodeDetailFragment is a detail view component and not a list selection activity, it's appropriate for it to have an empty implementation of deliverProgress().

Key findings:

  • EntitySelectActivity uses progress updates to show loading status for entity list processing
  • No other implementations were found that propagate progress
  • The fragment's purpose (showing entity details) doesn't require progress indication
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if other implementations of EntityLoaderListener propagate progress
rg -t java "implements.*EntityLoaderListener" -A 10
rg -t java "deliverProgress.*values.*" -A 5

Length of output: 3941

app/res/layout/entity_select_layout.xml (1)

Line range hint 56-75: Well-structured progress container layout!

The progress container implementation follows Android layout best practices with proper positioning and styling.

app/res/values/strings.xml (1)

459-461: Well-defined localization strings!

The new string resources are properly structured with:

  • Clear naming convention
  • Correct use of string formatting
  • Proper translatable attribute
app/src/org/commcare/activities/EntitySelectActivity.java (4)

169-170: LGTM: Progress text UI member variables

The addition of visibleView and progressTv member variables provides proper UI feedback during entity loading operations.


272-272: LGTM: Progress text initialization and visibility management

The progress text initialization in setupUI() and visibility management in setupUIFromAdapter() are properly implemented.

Also applies to: 361-361


481-481: LGTM: Entity loading progress updates

The progress text updates during entity loading provide clear user feedback, with appropriate throttling to prevent excessive UI updates.

Also applies to: 996-1003


904-906: LGTM: Progress text helper method

The setProgressText() helper method properly encapsulates progress text updates.

app/src/org/commcare/CommCareApplication.java (2)

392-392: LGTM: Entity cache Work Manager task cancellation

The PrimeEntityCacheHelper.cancelWork() call is properly integrated with the existing Work Manager task cleanup during user session termination.


802-802: LGTM: Entity cache Work Manager task scheduling

The PrimeEntityCacheHelper.schedulePrimeEntityCacheWorker() call is properly placed after user session initialization, alongside other background task scheduling.

factory.prepareEntities(full);
factory.printAndClearTraces("build");
return new Pair<>(full, references);
return entityLoaderHelper.loadEntities(nodeset[0], this);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validate array index usage in doInBackground()
doInBackground performs nodeset[0]. Always verify that the nodeset array is not empty or null to avoid ArrayIndexOutOfBoundsException.

+ if (nodeset == null || nodeset.length == 0) {
+     // handle error or return null
+ }
  return entityLoaderHelper.loadEntities(nodeset[0], this);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +57 to +79
private fun waitForCachePrimeWork(
entities: MutableList<Entity<TreeReference>>,
primeEntityCacheHelper: PrimeEntityCacheHelper
) {
val startTime = System.currentTimeMillis()
while (!completedCachePrime && (System.currentTimeMillis() - startTime) < TWO_MINUTES) {
// wait for it to be completed
try {
Thread.sleep(100)
} catch (_: InterruptedException) {
}
}
if (!completedCachePrime) {
Logger.log(LogTypes.TYPE_MAINTENANCE, "Still Waiting for cache priming work to complete")
// confirm if we are still priming in the worker. If yes, wait more
// otherwise recall prepareEntitiesInternal to re-evaluate the best thing to do
if (primeEntityCacheHelper.isInProgress() && primeEntityCacheHelper.isDetailInProgress(detail.id)) {
waitForCachePrimeWork(entities, primeEntityCacheHelper)
} else {
prepareEntitiesInternal(entities)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid long sleep loops
waitForCachePrimeWork uses Thread.sleep(100) in a loop, potentially hogging system resources. Consider using notifications, signals, or coroutines for more efficient waiting.

Comment on lines +69 to +73
fun primeEntityCache() {
checkPreConditions()
primeEntityCacheForApp(CommCareApplication.instance().commCarePlatform)
clearState()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential race condition after clearState()
PrimeEntityCache sets inProgress to true, but calls clearState() at the end which sets instance to null. If primeEntityCache() is called multiple times in quick succession, concurrency issues might arise. Consider more robust synchronization.

Comment on lines +11 to +19
override fun doWork(): Result {
try {
PrimeEntityCacheHelper.getInstance().primeEntityCache()
return Result.success()
} catch (e: Exception) {
Logger.exception("Error while priming cache in worker", e)
}
return Result.failure()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling and worker configuration

The error handling could be improved by:

  1. Catching specific exceptions
  2. Considering retry scenarios for recoverable errors
  3. Adding work constraints if needed (e.g., network, battery)
 override fun doWork(): Result {
     try {
         PrimeEntityCacheHelper.getInstance().primeEntityCache()
         return Result.success()
-    } catch (e: Exception) {
+    } catch (e: IOException) {
         Logger.exception("Error while priming cache in worker", e)
+        // Retry for network-related issues
+        return Result.retry()
+    } catch (e: Exception) {
+        Logger.exception("Unrecoverable error while priming cache in worker", e)
+        return Result.failure()
     }
-    return Result.failure()
 }

Consider adding work constraints in the scheduling code:

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresBatteryNotLow(true)
    .build()

@shubham1g5 shubham1g5 changed the base branch from master to refactorEntityLoaderTask December 19, 2024 11:13
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.

1 participant