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

[Proposal] Fallback Mechanisms #540

Closed
matt-ramotar opened this issue Mar 22, 2023 · 9 comments
Closed

[Proposal] Fallback Mechanisms #540

matt-ramotar opened this issue Mar 22, 2023 · 9 comments
Labels
Milestone

Comments

@matt-ramotar
Copy link
Collaborator

TLDR

Introduce Superstore and Warehouse to handle data retrieval and fallback mechanisms when a primary data source fails or returns an error.

Abstract

When a primary data source fails or returns an error, it is desirable to have a fallback mechanism in place. One proposed solution is to introduce two new classes, Warehouse and Superstore, to handle the fallback mechanism. The Warehouse contains the fallback data source and provides a simple API for fetching the data. The Superstore coordinates the Store and Warehouse, attempting to fetch data from the Store first, and if that fails, it will fetch data from the Warehouse.


Fallback Scenario Description Secondary Data Source
Network failure/server downtime When the primary data source (e.g., remote server or API) is temporarily unavailable, the application can switch to a local cache or hardcoded data to continue providing data to the users. Local cache, local databases, in-memory storage, file-based storage
API rate limiting When an application exceeds the rate limits of an API, it may need to switch to another data source (such as a local cache or a secondary API) until the rate limits reset. Local cache, secondary API
Version fallback In some cases, an application may need to fall back to an older version of the data or API if the current version is not supported, contains bugs, or is deprecated. Local cache, local databases
Data validation failure If the data received from the primary source fails validation or is incomplete, the application may use an alternative data source to ensure it provides accurate and complete information to users. Local cache, local databases, file-based storage
Geo-redundancy In case of regional network issues or data center outages, applications may automatically switch to a different data center or region to maintain availability. Secondary data center, CDN
Scheduled maintenance During planned maintenance of a primary data source, an application can switch to a backup or alternative source to minimize downtime and ensure continuous service. Backup data source, file-based storage
Data synchronization issues When data synchronization between multiple sources is delayed or interrupted, the application may switch to another source with more up-to-date information. Secondary data source, local databases, in-memory storage

Problem

For some use cases, it is desirable to have a fallback mechanism in place for when the Store encounters an error or is unable to fetch data. Currently, there is no built-in mechanism to handle such scenarios, leaving developers to implement custom solutions.

Alternatives considered

  1. Modify the existing Store class: We could modify the Store class to include an additional fallback mechanism. However, this approach would increase the complexity of the Store class and could break existing implementations.

  2. Create a new FallbackStore class: We could create a new FallbackStore class that inherits from the Store class and provides a fallback mechanism. This approach would keep the existing Store class intact but may result in code duplication and maintenance overhead.

  3. Provide guidance to users: By providing comprehensive documentation on handling fallback scenarios, developers can implement a fallback mechanism that fits their specific use case while avoiding unnecessary complexity or potential issues with modifying existing code.

Proposed solution

My proposal is to introduce two new classes, Warehouse and Superstore, to handle the fallback mechanism:

Warehouse

The Warehouse will contain hardcoded data or any other fallback data source. It will provide a simple API for fetching the fallback data.

interface Warehouse<Key : Any, Output : Any> {
    suspend fun get(key: Key): Output?
}

class HardcodedWarehouse<Key : Any, Output : Any>(private val data: Map<Key, Output>) : Warehouse<Key, Output> {
    override suspend fun get(key: Key): Output? {
        return data[key]
    }
}

Superstore

A class that coordinates the Store and Warehouse. The Superstore will first attempt to fetch data from the Store. If the Store fails or returns an error, the Superstore will fetch the data from the Warehouse.

class Superstore<Key : Any, Output : Any>(
    private val store: Store<Key, Output>,
    private val warehouses: List<Warehouse<Key, Output>>
) {
    suspend fun get(key: Key): Output {
        val storeResponse = store.stream(StoreReadRequest.fresh(key)).firstOrNull()
        if (storeResponse is StoreReadResponse.Data) {
            return storeResponse.value
        } else {
            for (warehouse in warehouses) {
                val fallbackData = warehouse.get(key)
                if (fallbackData != null) {
                    return fallbackData
                }
            }
        }
    }
}
@matt-ramotar
Copy link
Collaborator Author

@digitalbuddha Wanted to get this out of my head into an issue. My hunch is we will want to provide guidance through documentation and not change Store

@matt-ramotar matt-ramotar moved this from 🆕 Triage to 👀 In review in Store Roadmap Mar 22, 2023
@digitalbuddha
Copy link
Contributor

Yup this looks great. I think it opens up new use cases like managing feature flags where on first launch you don't have network and need the local default flag value. I'll read in way more detail but on first glance I love the direction you are going

@yigit
Copy link
Collaborator

yigit commented Apr 23, 2023

I think this looks good. I have 1 question on the first use case listed:
Network failure/server downtime
Given that we allow a local persistent storage already , how do we expect developers to coordinate the two? Does the warehouse fallback to that same storage or is it only expected to be used for cases where the local persistent backend of Store really doesn't have the data.

@matt-ramotar
Copy link
Collaborator Author

Thanks for reviewing! There is an outstanding feature request for enabling SOT as a fallback on failure to fetch fresh data (#394). My thinking has been fallbacks would be available regardless of whether the Store has data. It's possible to use the SOT as a warehouse. For example take a look at this test case. So a user could choose to provide the SOT as a warehouse, or for instance a secondary API and then SOT bindings.

@matt-ramotar
Copy link
Collaborator Author

Hmm - But yeah I did not consider:

  1. Conditional error handling - For example, if SOT is used as a warehouse, the user probably wants Superstore to swallow error when Store is empty. Currently we always throw if all fallbacks fail to find data.
  2. Conditional fall through - For example, if Store is empty, maybe a user wants to only fall back on a secondary API.

Thoughts?

@yigit
Copy link
Collaborator

yigit commented Apr 25, 2023

Hmm, it sounds a bit weird for fresh to fallback to SOT since it is all about not using SOT data. Otoh, I guess it would be convenient to provide a flag (maybe we want Store.fresh(fallbackToSourceOfTruth = false) ).

I feel like SOT serving as Warehouse is a bit weird. The data flow graph looks complicated, as in, there are 2 paths where it might fetch from SOT. If we are adding warehouse just for fresh, i think letting fresh accept a fallback parameter is a better solution.

I think there is still the case for the WareHouse when SOT is empty, but in that case, should this be something that goes under SOT instead of above Store? e.g. what if you could create SOT with a fallback Warehouse ? I'm wondering if that is a more accurate way to model it (and then Warehouse should not be using SOT). Or we could even create an API on SOT builder that lets you chain multiple SOTs?

e.g.

           val warehouse = SourceOfTruth.of<String, Page>(...)
           val sourceOfTruth = SourceOfTruth.of<String, Page>(
                nonFlowReader = { key -> pagesDatabase.get(key) },
                writer = { key, page -> pagesDatabase.put(key, page) },
                delete = null,
                deleteAll = null,
                warehouse = warehouse // or maybe call it fallback?
            )

that way, it can even be chained indefinitely :) I don't think this solves some options listed above (e..g. Geo case) but maybe the solution for that is to also allow chaining Fetchers? Both of these would also be just convenience APIs since these can all be done w/ coroutine operators (i think)

matt-ramotar added a commit that referenced this issue Apr 26, 2023
@matt-ramotar
Copy link
Collaborator Author

Thanks, very helpful! Chaining fetchers makes a lot of sense to me. WIP - 2d1cb0b. Will finish refactoring and documenting tomorrow

matt-ramotar added a commit that referenced this issue May 8, 2023
* Superstore

Signed-off-by: Matt Ramotar <[email protected]>

* Move to impl

Signed-off-by: Matt Ramotar <[email protected]>

* Add firstData

Signed-off-by: Matt Ramotar <[email protected]>

* Add Superstore factory

Signed-off-by: Matt Ramotar <[email protected]>

* Add README

Signed-off-by: Matt Ramotar <[email protected]>

* Add SuperstoreTests

Signed-off-by: Matt Ramotar <[email protected]>

* Move to util

Signed-off-by: Matt Ramotar <[email protected]>

* Rename to primary

Signed-off-by: Matt Ramotar <[email protected]>

* Address comments from @tsenggordon

Signed-off-by: Matt Ramotar <[email protected]>

* Format

Signed-off-by: Matt Ramotar <[email protected]>

* Cover fallback to SOT on fresh fail

Signed-off-by: Matt Ramotar <[email protected]>

* Refactor based on @yigit feedback in #540

Signed-off-by: Matt Ramotar <[email protected]>

* Enable Fetcher identification

Signed-off-by: Matt Ramotar <[email protected]>

* Cover failing fallback

Signed-off-by: Matt Ramotar <[email protected]>

* Remove superstore

Signed-off-by: Matt Ramotar <[email protected]>

* Format

Signed-off-by: Matt Ramotar <[email protected]>

* Remove logs

Signed-off-by: Matt Ramotar <[email protected]>

* Update README.md

Signed-off-by: Matt Ramotar <[email protected]>

* Add Proposal template (#523)

Signed-off-by: Matt Ramotar <[email protected]>

* Support Rx2 (#531)

* Add rx2 module

* Support Rx2

Signed-off-by: Matt Ramotar <[email protected]>

* Add unit tests

Signed-off-by: Matt Ramotar <[email protected]>

* Format

Signed-off-by: Matt Ramotar <[email protected]>

---------

Signed-off-by: Matt Ramotar <[email protected]>

* Superstore

Signed-off-by: Matt Ramotar <[email protected]>

* Remove superstore

Signed-off-by: Matt Ramotar <[email protected]>

* Remove ReactiveCircus/android-emulator-runner

Signed-off-by: Matt Ramotar <[email protected]>

* Fix Rx2 merge issues

Signed-off-by: Matt Ramotar <[email protected]>

* Send failure if no fallback

Signed-off-by: Matt Ramotar <[email protected]>

* Remove Fallback interface and FallbackResponse class

Signed-off-by: matt-ramotar <[email protected]>

* Document Fetcher.name

Signed-off-by: matt-ramotar <[email protected]>

---------

Signed-off-by: Matt Ramotar <[email protected]>
Signed-off-by: matt-ramotar <[email protected]>
@matt-ramotar
Copy link
Collaborator Author

Closed with #545

@digitalheir
Copy link

a bit weird for fresh to fallback to SOT since it is all about not using SOT data

I think the documentation for the ReadRequest functions should be updated to make this abundantly clear. It currently says:

refresh - if true then return fetcher (new) data as well (updating your caches)

To me the portion "as well" signals that the resulting flow emits first the cached value (if any), then the network data (if any) -- and potentially races the two sources? It's unclear as an end user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Done
Development

No branches or pull requests

4 participants