Code Monkey home page Code Monkey logo

unleash-client-rust's Introduction

Unleash API client for Rustlang

Unleash is a feature flag API system. This is a client for it to facilitate using the API to control features in Rust programs.

Client overview

The client is written using async rust. For communicating with the Unleash API surf or reqwest support is built in, or any async HTTP client can be provided by the user if they implement the thin trait used to abstract over the actual client.

Examples with async-std (feature 'surf-client') and tokio (feature 'reqwest-client') are in the examples/ in the source tree. See the API docs for more feature information.

To use it in a sync program, run an async executor and block_on() the relevant calls. As the client specification requires sending background metrics to the API, you will need to arrange to call the poll_for_updates method from a thread as demonstrated in examples/theads.rs

The unleash defined strategies are included, to support custom strategies use the ClientBuilder and call the strategy method to register your custom strategy memoization function.

The crate documentation should be consulted for more detail.

Configuration

The easiest way to get started with the Client is using the ClientBuilder. A simple example is provided:

let config = EnvironmentConfig::from_env()?;
let client = client::ClientBuilder::default()
    .interval(500)
    .into_client::<UserFeatures, reqwest::Client>(
        &config.api_url,
        &config.app_name,
        &config.instance_id,
        config.secret,
    )?;
client.register().await?;

The values required for the into_client method are described as follows (in order, as seen above):

  • api_url - The server URL to fetch toggles from.
  • app_name - The name of your application.
  • instance_id - A unique ID, ideally per run. A runtime generated UUID would be a sensible choice here.
  • authorization - An Unleash client secret, if set this will be passed as the authorization header.

While the above code shows the usage of the EnvironmentConfig, this isn't required and is provided as a convenient way of reading a data from the system environment variables.

EnvironmentConfig Property Environment Variable Required?
api_url UNLEASH_API_URL Yes
app_name UNLEASH_APP_NAME Yes
instance_id UNLEASH_INSTANCE_ID Yes
secret UNLEASH_CLIENT_SECRET No

Note that if you do use the EnvironmentConfig as a way of accessing the system variables, you'll need to ensure that all the environment variables marked as required in the above table are set, or a panic will be raised.

The ClientBuilder also has a few builder methods for setting properties which are assumed to have good defaults and generally do not require changing. If you do need to alter these properties you can invoke the following methods on the builder (as seen above with the interval).

Method Argument Description Default
interval u64 Sets the polling interval to the Unleash server, in milliseconds 15000ms
disable_metric_submission N/A Turns off the metrics submission to Unleash On
enable_string_features N/A By default the Rust SDK requires you to define an enum for feature resolution, turning this on will allow you to resolve your features by string types instead, through the use of the is_enabled_str method. Be warned that this is enforced by asserts and calling is_enabled_str without turning this on with result in a panic Off

Status

Core Unleash API features work, with Rust 1.60 or above. The MSRV for this project is weakly enforced: when a hard dependency raises its version, so will the minimum version tested against, but if older rust versions work for a user, that is not prevented. time in particular is known to enforce a 6-month compiler age, so regular increases with the minimum version tested against are expected.

Unimplemented Unleash specified features:

  • local serialised copy of toggles to survive restarts without network traffic.

Code of conduct

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Contributing

PR's on Github as normal please. Cargo test to run the test suite, rustfmt code before submitting. To run the functional test suite you need an Unleash API to execute against.

For instance, one way:

docker-compose up -d

Visit http://localhost:4242/ and log in with admin + unleash4all, then create a new API token at http://localhost:4242/admin/api/create-token for user admin, type Client.

Then run the test suite:

UNLEASH_API_URL=http://127.0.0.1:4242/api \
  UNLEASH_APP_NAME=fred UNLEASH_INSTANCE_ID=test \
  UNLEASH_CLIENT_SECRET="<tokenvalue>" \
  cargo test --features functional  -- --nocapture

or similar. The functional test suite looks for a manually setup set of features. E.g. log into the Unleash UI on port 4242 and create a feature called default.

unleash-client-rust's People

Contributors

amorken avatar chriswk avatar daveleek avatar frxstrem avatar kujeger avatar luisholanda avatar masonj5n avatar mstyura avatar rbtcollins avatar sighphyre avatar sjaanus avatar teqm avatar thomasheartman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

unleash-client-rust's Issues

feat: Use HTTP statuses to backoff when needed

Describe the feature request

In order to reduce load on servers, as well as save our users some bandwidth, I'd like for this SDK to react to http status codes, and not just keep requesting at the same frequency if the server is telling it to chill out.

Background

Part of a cross-SDK initiative to make all our SDKs respect http statuses in order to save ourselves and our users for bandwidth/cpu usage that adds no value to either the server or the client.

Solution suggestions

Unleash/unleash-client-node#537 follows the correct pattern. Use 404, 429 or 50x statuses to reduce polling frequency. On 401 or 403 log and don't keep polling - your user probably needs to update their key before there's any point in continuing to hit the server.

Make poll_for_updates Send

Describe the feature request

Currently poll_for_updates is !Send. It would be nice if it could be made Send.

Background

Because poll_for_updates is !Send, it can't be spawned using tokio::spawn or other similar methods and must be on either an actual thread or a local task set. This is not super ergonomic: a common pattern in async applications seems to be spawning persistent tasks like poll_for_updates with tokio::spawn in a 'fire and forget' manner.

Solution suggestions

I haven't looked at the code yet to see if making the function Send is an easy or hard task.

unknown variant `SEMVER_LT`, expected `IN` or `NOT_IN`"

Describe the bug

Fetching feature flags fails due to the following error:

Err(reqwest::Error { kind: Decode, source: Error("unknown variant `SEMVER_LT`, expected `IN` or `NOT_IN`", line: 1, column: 44320) })

This means its not possible to use the client at all with those constraints defined in the project. It should be possible to use the client, even if those strategies are not supported.

Steps to reproduce the bug

No response

Expected behavior

No response

Logs, error output, etc.

No response

Screenshots

No response

Additional context

No response

Unleash version

No response

Subscription type

None

Hosting type

None

SDK information (language and version)

No response

Implement Global Segments

Is there an existing issue for this?

  • I have searched the existing issues

Describe the new feature

Just a feature that needs to be implemented - Global Segments. Unleash v4.13 supports enhanced responses for global segments, it would be great if this SDK can make use of this.

Background

Segments are effectively a way for Unleash users to define a list of constraints in such a way that makes them reusable across toggles without manually copying the constraint across to another toggle. Segments have two modes of operation, from the SDK's perspective, the inline mode will have no impact, segments will be remapped on the server side into constraints on the toggle information, no changes need to be . The second mode, global segments, requires that the SDK both opt in and handle the response differently. The handling should effectively result in unpacking the segments referenced in the feature strategies into a set of constraints. The changes required are described below.

Solution suggestions

Control Header

The SDK needs to pass up a Unleash-Client-Spec header with a semver value greater or equal to 4.2.0 (i.e. be greater or equal to the version of the unleash client spec tests where global segments are described) when hitting the get toggles endpoint on the Unleash server. This will enable the Unleash server to respond with the enhanced format

Example of the difference between enhanced and standard format:

Standard Format (default)

{
   "version": 2,
   "features": [  
       {
           "strategies": [
               {
                   "name": "flexibleRollout",
                   "constraints": [
                       {
                           "values": [
                               "31"
                           ],
                           "inverted": false,
                           "operator": "IN",
                           "contextName": "appName",
                           "caseInsensitive": false
                       }
                   ],          
                   "parameters": {
                       "groupId": "Test1",
                       "rollout": "100",
                       "stickiness": "default"
                   }
               }
           ],
           "name": "Test1"
       },
       {
           "strategies": [
               {
                   "name": "flexibleRollout",
                   "constraints": [
                       {
                           "values": [
                               "31"
                           ],
                           "inverted": false,
                           "operator": "IN",
                           "contextName": "appName",
                           "caseInsensitive": false
                       }
                   ],          
                   "parameters": {
                       "groupId": "Test2",
                       "rollout": "100",
                       "stickiness": "default"
                   }
               }
           ],
           "name": "Test2"
       }    
   ],
   "query": {
       "environment": "default"
   }
}

Enhanced Format (requires opt in)

{
   "version": 2,
   "features": [   
       {
           "strategies": [
               {
                   "name": "flexibleRollout",
                   "constraints": [],
                   "parameters": {
                       "groupId": "Test1",
                       "rollout": "100",
                       "stickiness": "default"
                   },
                   "segments": [
                       1
                   ]
               }
           ],
           "name": "Test1"
       },
       {
           "strategies": [
               {
                   "name": "flexibleRollout",
                   "constraints": [],
                   "parameters": {
                       "groupId": "Test2",
                       "rollout": "100",
                       "stickiness": "default"
                   },
                   "segments": [
                       1
                   ]
               }
           ],
           "name": "Test2"
       }     
   ],
   "query": {
       "environment": "default"
   },
   "segments": [
       {
           "id": 1,           
           "constraints": [
               {
                   "values": [
                       "31"
                   ],
                   "inverted": false,
                   "operator": "IN",
                   "contextName": "appName",
                   "caseInsensitive": false
               }
           ]           
       }
   ]
}

The relevant changes between the two formats are that in the enhanced format the segments are defined once as a global list and referenced within the strategy on the toggle by its ID. What's important to note is that the two above packets should be
handled identically, they reference the same toggle state.

Considerations

  • Global segments are intended to handle large datasets, how large has not been formally specified yet but expectations are around 1 000 to 10 000 segments across 1 000 toggles. As a result, time and space complexity of the implementations needs to be considered.
  • In the case of global segments, if the mapping from segment id to segment is incomplete i.e. a segment id is referenced in a toggle strategy that doesn’t map back to the global segments list, then the toggle should be evaluated to false. This is enforced through one of the client specification tests in v4.2.0 of the client spec
  • A reference implementation is provided in node JS: Unleash/unleash-client-node#329 (note that this doesn't include the header, that can be seen here: Unleash/unleash-client-node#335)

Add Tokio ecossystem support

Describe the feature request

Give the user the option to use a tokio-based HTTP stack.

Background

At $DAILY_JOB, our Rust codebase is 100% tokio-based, using the unleash-api-client crate as dependency added 100+ crates to our Cargo.lock, which is bad.

Checking the added dependencies, many of them are crates of the async-std ecossystem, which we would not need to pull if we had the option to use a tokio-based HTTP stack.

Solution suggestions

We could gate both the async-std and a tokio-based http (e.g. hyper) implementations behind cargo features (keeping the async-std enabled by default).

From what I understood of the implementation, the only method that does HTTP calls is Client::poll_for_updates (edit: Client::register also does a HTTP call). If so, we would to change the code to something like:

impl Client<F> {
	pub async fn poll_for_updates(&self) {
		#[cfg(feature = "async-std")]
		self.poll_for_updates_async_std().await;
		#[cfg(feature = "tokio")]
		self.poll_for_updates_tokio().await;
	}

	#[cfg(feature = "async-std")]
	async fn poll_for_updates_async_std(&self) {
		... // current implementation
	}
	
	#[cfg(feature = "tokio")]
	async fn poll_for_updates_tokio(&self) {
		...
	}
}

I'm open to work on fixing this issue.

custom strategies that aren't configured into the client are hard to debug

Describe the bug

This log shows a test with a custom strategy that hasn't been enabled using .strategy(): it requires a TRACE level log statement to even tell that that is whats happened.

2022-09-27T20:12:34.518Z TRACE [unleash_api_client::client] memoize: start with 1 features
2022-09-27T20:12:34.518Z TRACE [unleash_api_client::client] memoize: swapped memoized state in
2022-09-27T20:12:34.518Z DEBUG [unleash_api_client::client] poll: waiting 500ms
2022-09-27T20:12:35.013Z TRACE [unleash_api_client::client] is_enabled: feature project_test default false, context Some(Context { user_id: None, session_id: None, remote_address: None, properties: {"projectId": "project", "cluster": "clustername"}, app_name: "app", environment: "clustername" })
2022-09-27T20:12:35.013Z TRACE [unleash_api_client::client] is_enabled: feature project_test default false, context Some(Context { user_id: None, session_id: None, remote_address: None, properties: {"projectId": "project", "cluster": "clustername"}, app_name: "app", environment: "clustername" })
2022-09-27T20:12:35.013Z TRACE [unleash_api_client::client] is_enabled: feature project_test has no strategies: enabling
feature 'project_test' is true

We should probably log the silent dropping of unknown strategies much more visibly.

Steps to reproduce the bug

No response

Expected behavior

No response

Logs, error output, etc.

No response

Screenshots

No response

Additional context

No response

Unleash version

No response

Subscription type

No response

Hosting type

No response

SDK information (language and version)

No response

Build of `0.4.0` failed

Rust Version

rustc 1.47.0 (18bf6b4f0 2020-10-07)

Command to check

cargo build

Result

error[E0107]: wrong number of type arguments: expected 0, found 1
  --> src/http.rs:14:26
   |
14 |     client: surf::Client<C>,
   |                          ^ unexpected type argument

error[E0107]: wrong number of type arguments: expected 0, found 1
  --> src/http.rs:36:62
   |
36 |     pub fn get(&self, uri: impl AsRef<str>) -> surf::Request<C> {
   |                                                              ^ unexpected type argument

error[E0107]: wrong number of type arguments: expected 0, found 1
  --> src/http.rs:50:63
   |
50 |     pub fn post(&self, uri: impl AsRef<str>) -> surf::Request<C> {
   |                                                               ^ unexpected type argument

error: aborting due to 3 previous errors

For more information about this error, try `rustc --explain E0107`.
error: could not compile `unleash-api-client`.

Solution

Tag 0.5.0 from master.

Client should handle 502 errors

Describe the bug

The unleash server can return the following error when polling for features (poll_for_update()):

\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<title>502 Server Error</title>\n</head>\n<body text=#000000 bgcolor=#ffffff>\n<h1>Error: Server Error</h1>\n<h2>The server encountered a temporary error and could not complete your request.<p>Please try again in 30 seconds.</h2>\n<h2></h2>\n</body></html>\n

The client currently naively tries to convert the returned body into json, fails, then dumps the error and proceeds as if an unrecoverable error happened, by not updating the features until the next poll interval/

This should be easily fixable by inspecting the response's status code and reacting to a 502, before converting it into a json.

Steps to reproduce the bug

Unfortunately I am unsure as to how to reproduce the error, as it may have something to do with specific server configurations or even the environment. However, it might be possible to mock the response from the server.

Expected behavior

Unleash client should probably retry the request as said by the server.

Logs, error output, etc.

No response

Screenshots

No response

Additional context

No response

Unleash version

Client: 0.6.1 alpha
Server: 3.17.6

Subscription type

Open source

Hosting type

Self-hosted

SDK information (language and version)

No response

poll_for_updates discussion

Describe the feature request

It would be helpful to understand the design decision not to automatically support execution of poll_for_updates.

Is the library trying to remain as agnostic as possible to different execution models - std::thread tokio async/await etc?

Or was there a total misunderstanding and we are able to configure the library to fetch updates?

Background

No response

Solution suggestions

No response

More informative warn logs

Describe the feature request

In poll_for_updates, errors are consumed and a fixed log message is printed instead. For example, if the HTTP request fails it simply prints warn!("poll: failed to retrieve features"). This is unhelpful for debugging. It'd be helpful to print the particulars of the error.

Background

I am currently attempting to use unleash-api-client in a cloud-hosted project. It has started sporadically failing, but without information on the particulars I can't easily debug it.

Solution suggestions

It'd be a case-by-case basis, but generally would just need to include a debug-formatted version of the originating error.

"413 Content Too Large" when submitting metrics

Describe the bug

Hi!

Testing out this Rust sdk and it works well for getting feature toggle status, but it always fails when submitting metrics, with this in the log:

WARN unleash_api_client::client > poll: error uploading feature metrics

after adding some debug lines in the library, it turns out that the actual error (that unfortunately gets swallowed in post_json) is along the lines of

413 Content Too Large
Error
Payload Too Large

Our unleash installation (at FINN.no) has a large amount of features, and digging through the code, it looks like the rust client tries to upload metrics for every single feature that exists -- even if I've only configured a UserFeatures enum with a single variant -- resulting in over 150KB of data.

Is this intentional and required? Naively I'd have guessed that it only made sens to upload metrics for the features defined in the enum passed to the client builder.

Steps to reproduce the bug

No response

Expected behavior

No response

Logs, error output, etc.

No response

Screenshots

No response

Additional context

No response

Unleash version

3.17

Subscription type

Open source

Hosting type

Self-hosted

SDK information (language and version)

No response

Performance with unknown features

Unknown features are meant to use rcu to insert a thunk on demand for unknown features to make the use of an unknown feature a once-per-polling-cycle overhead. Benchmarking shows that this isn't working: the rcu slow code path is being hit every time, and performance tanks: pathologically with 32 threads of totally unknown features we get:

Benchmarking across 32 threads with 50000 iterations per thread
...
batch/parallel unknown-features
                        time:   [1.7025 s 1.7239 s 1.7392 s]
                        thrpt:  [919.98 Kelem/s 928.13 Kelem/s 939.80 Kelem/s]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.