Skip to content

Commit 445ab74

Browse files
authored
feat: Add support for migrations (#90)
1 parent 9d1fbd2 commit 445ab74

File tree

17 files changed

+2954
-67
lines changed

17 files changed

+2954
-67
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: Run CI
22
on:
33
push:
4-
branches: [ main ]
4+
branches: [ main, 'feat/**' ]
55
paths-ignore:
66
- '**.md' # Do not need to run CI for markdown changes.
77
pull_request:
8-
branches: [ main ]
8+
branches: [ main, 'feat/**' ]
99
paths-ignore:
1010
- '**.md'
1111

@@ -57,4 +57,4 @@ jobs:
5757
run: sudo apt-get update && sudo apt-get install -y musl-tools
5858

5959
- name: Build
60-
run: TARGET_CC=musl-gcc RUSTFLAGS="-C linker=musl-gcc" cargo build --release --target=x86_64-unknown-linux-musl
60+
run: TARGET_CC=musl-gcc RUSTFLAGS="-C linker=musl-gcc" cargo build --release --target=x86_64-unknown-linux-musl -p launchdarkly-server-sdk

contract-tests/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ futures = "0.3.12"
1717
hyper = { version = "0.14.19", features = ["client"] }
1818
hyper-rustls = { version = "0.24.1" , optional = true, features = ["http2"]}
1919
hyper-tls = { version = "0.5.0", optional = true }
20+
reqwest = { version = "0.12.4", features = ["default", "blocking", "json"] }
21+
async-mutex = "1.4.0"
2022

2123
[features]
2224
default = ["rustls"]

contract-tests/src/client_entity.rs

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use launchdarkly_server_sdk::{Context, ContextBuilder, MultiContextBuilder, Reference};
1+
use futures::future::FutureExt;
2+
use launchdarkly_server_sdk::{
3+
Context, ContextBuilder, MigratorBuilder, MultiContextBuilder, Reference,
4+
};
5+
use std::sync::Arc;
26
use std::time::Duration;
37

48
const DEFAULT_POLLING_BASE_URL: &str = "https://sdk.launchdarkly.com";
@@ -12,7 +16,8 @@ use launchdarkly_server_sdk::{
1216
};
1317

1418
use crate::command_params::{
15-
ContextBuildParams, ContextConvertParams, ContextParam, ContextResponse, SecureModeHashResponse,
19+
ContextBuildParams, ContextConvertParams, ContextParam, ContextResponse,
20+
MigrationOperationResponse, MigrationVariationResponse, SecureModeHashResponse,
1621
};
1722
use crate::HttpsConnector;
1823
use crate::{
@@ -24,7 +29,7 @@ use crate::{
2429
};
2530

2631
pub struct ClientEntity {
27-
client: Client,
32+
client: Arc<Client>,
2833
}
2934

3035
impl ClientEntity {
@@ -131,10 +136,15 @@ impl ClientEntity {
131136
client.start_with_default_executor();
132137
client.wait_for_initialization(Duration::from_secs(5)).await;
133138

134-
Ok(Self { client })
139+
Ok(Self {
140+
client: Arc::new(client),
141+
})
135142
}
136143

137-
pub fn do_command(&self, command: CommandParams) -> Result<Option<CommandResponse>, String> {
144+
pub async fn do_command(
145+
&self,
146+
command: CommandParams,
147+
) -> Result<Option<CommandResponse>, String> {
138148
match command.command.as_str() {
139149
"evaluate" => Ok(Some(CommandResponse::EvaluateFlag(
140150
self.evaluate(command.evaluate.ok_or("Evaluate params should be set")?),
@@ -211,6 +221,132 @@ impl ClientEntity {
211221
},
212222
)))
213223
}
224+
"migrationVariation" => {
225+
let params = command
226+
.migration_variation
227+
.ok_or("migrationVariation params should be set")?;
228+
229+
let (stage, _) = self.client.migration_variation(
230+
&params.context,
231+
&params.key,
232+
params.default_stage,
233+
);
234+
235+
Ok(Some(CommandResponse::MigrationVariation(
236+
MigrationVariationResponse { result: stage },
237+
)))
238+
}
239+
"migrationOperation" => {
240+
let params = command
241+
.migration_operation
242+
.ok_or("migrationOperation params should be set")?;
243+
244+
let mut builder = MigratorBuilder::new(self.client.clone());
245+
246+
builder = builder
247+
.read_execution_order(params.read_execution_order)
248+
.track_errors(params.track_errors)
249+
.track_latency(params.track_latency)
250+
.read(
251+
|payload: &Option<String>| {
252+
let old_endpoint = params.old_endpoint.clone();
253+
async move {
254+
let result = send_payload(&old_endpoint, payload.clone()).await;
255+
match result {
256+
Ok(r) => Ok(Some(r)),
257+
Err(e) => Err(e),
258+
}
259+
}
260+
.boxed()
261+
},
262+
|payload| {
263+
let new_endpoint = params.new_endpoint.clone();
264+
async move {
265+
let result = send_payload(&new_endpoint, payload.clone()).await;
266+
match result {
267+
Ok(r) => Ok(Some(r)),
268+
Err(e) => Err(e),
269+
}
270+
}
271+
.boxed()
272+
},
273+
if params.track_consistency {
274+
Some(|lhs, rhs| lhs == rhs)
275+
} else {
276+
None
277+
},
278+
)
279+
.write(
280+
|payload| {
281+
let old_endpoint = params.old_endpoint.clone();
282+
async move {
283+
let result = send_payload(&old_endpoint, payload.clone()).await;
284+
match result {
285+
Ok(r) => Ok(Some(r)),
286+
Err(e) => Err(e),
287+
}
288+
}
289+
.boxed()
290+
},
291+
|payload| {
292+
let new_endpoint = params.new_endpoint.clone();
293+
async move {
294+
let result = send_payload(&new_endpoint, payload.clone()).await;
295+
match result {
296+
Ok(r) => Ok(Some(r)),
297+
Err(e) => Err(e),
298+
}
299+
}
300+
.boxed()
301+
},
302+
);
303+
304+
let mut migrator = builder.build().expect("builder failed");
305+
match params.operation {
306+
launchdarkly_server_sdk::Operation::Read => {
307+
let result = migrator
308+
.read(
309+
&params.context,
310+
params.key,
311+
params.default_stage,
312+
params.payload,
313+
)
314+
.await;
315+
316+
let payload = match result.result {
317+
Ok(payload) => payload.unwrap_or_else(|| "success".into()),
318+
Err(e) => e.to_string(),
319+
};
320+
321+
Ok(Some(CommandResponse::MigrationOperation(
322+
MigrationOperationResponse { result: payload },
323+
)))
324+
}
325+
launchdarkly_server_sdk::Operation::Write => {
326+
let result = migrator
327+
.write(
328+
&params.context,
329+
params.key,
330+
params.default_stage,
331+
params.payload,
332+
)
333+
.await;
334+
335+
let payload = match result.authoritative.result {
336+
Ok(payload) => payload.unwrap_or_else(|| "success".into()),
337+
Err(e) => e.to_string(),
338+
};
339+
340+
Ok(Some(CommandResponse::MigrationOperation(
341+
MigrationOperationResponse { result: payload },
342+
)))
343+
}
344+
_ => Err(format!(
345+
"Invalid operation requested: {:?}",
346+
params.operation
347+
)),
348+
}
349+
}
214350
command => Err(format!("Invalid command requested: {}", command)),
215351
}
216352
}
@@ -430,3 +566,25 @@ impl Drop for ClientEntity {
430566
self.client.close();
431567
}
432568
}
569+
570+
async fn send_payload(endpoint: &str, payload: Option<String>) -> Result<String, String>
571+
where
572+
{
573+
let client = reqwest::Client::new();
574+
let response = client
575+
.post(endpoint)
576+
.body(payload.unwrap_or_default())
577+
.send()
578+
.await
579+
.expect("sending request to SDK test harness");
580+
581+
if response.status().is_success() {
582+
let body = response.text().await.expect("read harness response body");
583+
Ok(body.to_string())
584+
} else {
585+
Err(format!(
586+
"requested failed with status code {}",
587+
response.status()
588+
))
589+
}
590+
}

contract-tests/src/command_params.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use launchdarkly_server_sdk::{AttributeValue, Context, FlagDetail, FlagValue, Reason};
1+
use launchdarkly_server_sdk::{
2+
AttributeValue, Context, ExecutionOrder, FlagDetail, FlagValue, Operation, Reason, Stage,
3+
};
24
use serde::{self, Deserialize, Serialize};
35
use std::collections::HashMap;
46

@@ -9,6 +11,8 @@ pub enum CommandResponse {
911
EvaluateAll(EvaluateAllFlagsResponse),
1012
ContextBuildOrConvert(ContextResponse),
1113
SecureModeHash(SecureModeHashResponse),
14+
MigrationVariation(MigrationVariationResponse),
15+
MigrationOperation(MigrationOperationResponse),
1216
}
1317

1418
#[derive(Deserialize, Debug)]
@@ -22,6 +26,8 @@ pub struct CommandParams {
2226
pub context_build: Option<ContextBuildParams>,
2327
pub context_convert: Option<ContextConvertParams>,
2428
pub secure_mode_hash: Option<SecureModeHashParams>,
29+
pub migration_variation: Option<MigrationVariationParams>,
30+
pub migration_operation: Option<MigrationOperationParams>,
2531
}
2632

2733
#[derive(Deserialize, Debug)]
@@ -130,3 +136,39 @@ pub struct SecureModeHashParams {
130136
pub struct SecureModeHashResponse {
131137
pub result: String,
132138
}
139+
140+
#[derive(Deserialize, Debug)]
141+
#[serde(rename_all = "camelCase")]
142+
pub struct MigrationVariationParams {
143+
pub key: String,
144+
pub context: Context,
145+
pub default_stage: Stage,
146+
}
147+
148+
#[derive(Serialize, Debug, Clone)]
149+
#[serde(rename_all = "camelCase")]
150+
pub struct MigrationVariationResponse {
151+
pub result: Stage,
152+
}
153+
154+
#[derive(Deserialize, Debug)]
155+
#[serde(rename_all = "camelCase")]
156+
pub struct MigrationOperationParams {
157+
pub key: String,
158+
pub context: Context,
159+
pub default_stage: Stage,
160+
pub read_execution_order: ExecutionOrder,
161+
pub operation: Operation,
162+
pub old_endpoint: String,
163+
pub new_endpoint: String,
164+
pub payload: Option<String>,
165+
pub track_latency: bool,
166+
pub track_errors: bool,
167+
pub track_consistency: bool,
168+
}
169+
170+
#[derive(Serialize, Debug, Clone)]
171+
#[serde(rename_all = "camelCase")]
172+
pub struct MigrationOperationResponse {
173+
pub result: String,
174+
}

contract-tests/src/main.rs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ mod command_params;
44
use crate::command_params::CommandParams;
55
use actix_web::error::{ErrorBadRequest, ErrorInternalServerError};
66
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder, Result};
7+
use async_mutex::Mutex;
78
use client_entity::ClientEntity;
89
use futures::executor;
910
use hyper::client::HttpConnector;
1011
use launchdarkly_server_sdk::Reference;
1112
use serde::{self, Deserialize, Serialize};
1213
use std::collections::{HashMap, HashSet};
13-
use std::sync::{mpsc, Mutex};
14+
use std::sync::mpsc;
1415
use std::thread;
1516

1617
#[derive(Serialize)]
@@ -106,6 +107,8 @@ async fn status() -> impl Responder {
106107
"inline-context".to_string(),
107108
"anonymous-redaction".to_string(),
108109
"omit-anonymous-contexts".to_string(),
110+
"migrations".to_string(),
111+
"event-sampling".to_string(),
109112
],
110113
})
111114
}
@@ -132,17 +135,8 @@ async fn create_client(
132135
Err(e) => return HttpResponse::InternalServerError().body(format!("{}", e)),
133136
};
134137

135-
let mut counter = match app_state.counter.lock() {
136-
Ok(c) => c,
137-
Err(_) => return HttpResponse::InternalServerError().body("Unable to retrieve counter"),
138-
};
139-
140-
let mut entities = match app_state.client_entities.lock() {
141-
Ok(h) => h,
142-
Err(_) => {
143-
return HttpResponse::InternalServerError().body("Unable to lock client_entities")
144-
}
145-
};
138+
let mut counter = app_state.counter.lock().await;
139+
let mut entities = app_state.client_entities.lock().await;
146140

147141
*counter += 1;
148142
let client_resource = match req.url_for("client_path", [counter.to_string()]) {
@@ -171,17 +165,15 @@ async fn do_command(
171165

172166
let client_id = client_id.parse::<u32>().map_err(ErrorInternalServerError)?;
173167

174-
let entities = app_state
175-
.client_entities
176-
.lock()
177-
.expect("Client entities cannot be locked");
168+
let entities = app_state.client_entities.lock().await;
178169

179170
let entity = entities
180171
.get(&client_id)
181172
.ok_or_else(|| ErrorBadRequest("The specified client does not exist"))?;
182173

183174
let result = entity
184175
.do_command(command_params.into_inner())
176+
.await
185177
.map_err(ErrorBadRequest)?;
186178

187179
match result {
@@ -197,14 +189,8 @@ async fn stop_client(req: HttpRequest, app_state: web::Data<AppState>) -> HttpRe
197189
Err(_) => return HttpResponse::BadRequest().body("Unable to parse client id"),
198190
};
199191

200-
match app_state.client_entities.lock() {
201-
Ok(mut entities) => {
202-
entities.remove(&client_id);
203-
}
204-
Err(_) => {
205-
return HttpResponse::InternalServerError().body("Unable to retrieve handles")
206-
}
207-
};
192+
let mut entities = app_state.client_entities.lock().await;
193+
entities.remove(&client_id);
208194

209195
HttpResponse::NoContent().finish()
210196
} else {

launchdarkly-server-sdk/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ moka = { version = "0.12.1", features = ["sync"] }
3434
uuid = {version = "1.2.2", features = ["v4"] }
3535
hyper = { version = "0.14.19", features = ["client", "http1", "http2", "tcp"] }
3636
hyper-rustls = { version = "0.24.1" , optional = true}
37+
rand = "0.8"
3738

3839
[dev-dependencies]
3940
maplit = "1.0.1"
@@ -43,6 +44,8 @@ tokio = { version = "1.17.0", features = ["macros", "time"] }
4344
test-case = "3.2.1"
4445
mockito = "1.2.0"
4546
assert-json-diff = "2.0.2"
47+
async-std = "1.12.0"
48+
reqwest = { version = "0.12.4", features = ["json"] }
4649

4750
[features]
4851
default = ["rustls"]

0 commit comments

Comments
 (0)