Skip to content

Commit 1f15c95

Browse files
authored
Implement the CCT prioritizer and an integration test (#2701)
* Implement the CCT prioritizer * Add support for high priority uploads * Add an integration test, demonstrating usage of CCT * Update the podspec
1 parent db3da2e commit 1f15c95

File tree

9 files changed

+545
-8
lines changed

9 files changed

+545
-8
lines changed

GoogleDataTransportCCTSupport.podspec

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Support library to provide event prioritization and uploading for the GoogleData
4646
test_spec.requires_app_host = false
4747
test_spec.source_files = 'GoogleDataTransportCCTSupport/Tests/Unit/**/*.{h,m}'
4848
test_spec.resources = ['GoogleDataTransportCCTSupport/Tests/Data/**/*']
49+
test_spec.dependency 'GCDWebServer'
50+
end
51+
52+
s.test_spec 'Tests-Integration' do |test_spec|
53+
test_spec.requires_app_host = false
54+
test_spec.source_files = 'GoogleDataTransportCCTSupport/Tests/Integration/**/*.{h,m}'
55+
test_spec.resources = ['GoogleDataTransportCCTSupport/Tests/Data/**/*']
4956
end
5057

5158
end

GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTPrioritizer.m

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ + (void)load {
2525
[[GDTRegistrar sharedInstance] registerPrioritizer:prioritizer target:kGDTTargetCCT];
2626
}
2727

28-
/** Creates and returns the singleton instance of this class.
29-
*
30-
* @return The singleton instance of this class.
31-
*/
3228
+ (instancetype)sharedInstance {
3329
static GDTCCTPrioritizer *sharedInstance;
3430
static dispatch_once_t onceToken;
@@ -67,6 +63,12 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition
6763
GDTUploadPackage *package = [[GDTUploadPackage alloc] init];
6864
dispatch_sync(_queue, ^{
6965
NSSet<GDTStoredEvent *> *logEventsThatWillBeSent;
66+
// A high priority event effectively flushes all events to be sent.
67+
if ((conditions & GDTUploadConditionHighPriority) == GDTUploadConditionHighPriority) {
68+
package.events = self.events;
69+
return;
70+
}
71+
7072
if ((conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData) {
7173
logEventsThatWillBeSent = [self logEventsOkToSendOnWifi];
7274
} else {

GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/GDTCCTUploader.m

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@
1717
#import "GDTCCTUploader.h"
1818

1919
#import <GoogleDataTransport/GDTRegistrar.h>
20+
#import <nanopb/pb.h>
21+
#import <nanopb/pb_decode.h>
22+
#import <nanopb/pb_encode.h>
23+
24+
#import "GDTCCTNanopbHelpers.h"
25+
#import "GDTCCTPrioritizer.h"
26+
#import "cct.nanopb.h"
27+
28+
@interface GDTCCTUploader ()
29+
30+
// Redeclared as readwrite.
31+
@property(nullable, nonatomic, readwrite) NSURLSessionUploadTask *currentTask;
32+
33+
@end
2034

2135
@implementation GDTCCTUploader
2236

@@ -34,8 +48,94 @@ + (instancetype)sharedInstance {
3448
return sharedInstance;
3549
}
3650

37-
- (void)uploadPackage:(nonnull GDTUploadPackage *)package
38-
onComplete:(nonnull GDTUploaderCompletionBlock)onComplete {
51+
- (instancetype)init {
52+
self = [super init];
53+
if (self) {
54+
_uploaderQueue = dispatch_queue_create("com.google.GDTCCTUploader", DISPATCH_QUEUE_SERIAL);
55+
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
56+
_uploaderSession = [NSURLSession sessionWithConfiguration:config];
57+
}
58+
return self;
59+
}
60+
61+
- (NSURL *)defaultServerURL {
62+
static NSURL *defaultServerURL;
63+
static dispatch_once_t onceToken;
64+
dispatch_once(&onceToken, ^{
65+
// These strings should be interleaved to construct the real URL. This is just to (hopefully)
66+
// fool github URL scanning bots.
67+
const char *p1 = "hts/frbslgiggolai.o/0clgbth";
68+
const char *p2 = "tp:/ieaeogn.ogepscmvc/o/ac";
69+
const char defaultURL[54] = {
70+
p1[0], p2[0], p1[1], p2[1], p1[2], p2[2], p1[3], p2[3], p1[4], p2[4], p1[5],
71+
p2[5], p1[6], p2[6], p1[7], p2[7], p1[8], p2[8], p1[9], p2[9], p1[10], p2[10],
72+
p1[11], p2[11], p1[12], p2[12], p1[13], p2[13], p1[14], p2[14], p1[15], p2[15], p1[16],
73+
p2[16], p1[17], p2[17], p1[18], p2[18], p1[19], p2[19], p1[20], p2[20], p1[21], p2[21],
74+
p1[22], p2[22], p1[23], p2[23], p1[24], p2[24], p1[25], p2[25], p1[26], '\0'};
75+
defaultServerURL = [NSURL URLWithString:[NSString stringWithUTF8String:defaultURL]];
76+
});
77+
return defaultServerURL;
78+
}
79+
80+
- (void)uploadPackage:(GDTUploadPackage *)package
81+
onComplete:(GDTUploaderCompletionBlock)onComplete {
82+
dispatch_async(_uploaderQueue, ^{
83+
NSAssert(!self->_currentTask, @"An upload shouldn't be initiated with another in progress.");
84+
NSURL *serverURL = self.serverURL ? self.serverURL : [self defaultServerURL];
85+
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:serverURL];
86+
request.HTTPMethod = @"POST";
87+
88+
id completionHandler = ^(NSData *_Nullable data, NSURLResponse *_Nullable response,
89+
NSError *_Nullable error) {
90+
NSAssert(!error, @"There should be no errors uploading events: %@", error);
91+
if (onComplete) {
92+
GDTClock *nextUploadTime;
93+
NSError *decodingError;
94+
gdt_cct_LogResponse response = GDTCCTDecodeLogResponse(data, &decodingError);
95+
if (!decodingError && response.has_next_request_wait_millis) {
96+
nextUploadTime = [GDTClock clockSnapshotInTheFuture:response.next_request_wait_millis];
97+
} else {
98+
// 15 minutes from now.
99+
nextUploadTime = [GDTClock clockSnapshotInTheFuture:15 * 60 * 1000];
100+
}
101+
pb_release(gdt_cct_LogResponse_fields, &response);
102+
onComplete(kGDTTargetCCT, nextUploadTime, error);
103+
}
104+
self.currentTask = nil;
105+
};
106+
NSData *requestProtoData = [self constructRequestProtoFromPackage:(GDTUploadPackage *)package];
107+
self.currentTask = [self.uploaderSession uploadTaskWithRequest:request
108+
fromData:requestProtoData
109+
completionHandler:completionHandler];
110+
[self.currentTask resume];
111+
});
112+
}
113+
114+
#pragma mark - Private helper methods
115+
116+
/** Constructs data given an upload package.
117+
*
118+
* @param package The upload package used to construct the request proto bytes.
119+
* @return Proto bytes representing a gdt_cct_LogRequest object.
120+
*/
121+
- (nonnull NSData *)constructRequestProtoFromPackage:(GDTUploadPackage *)package {
122+
// Segment the log events by log type.
123+
NSMutableDictionary<NSString *, NSMutableSet<GDTStoredEvent *> *> *logMappingIDToLogSet =
124+
[[NSMutableDictionary alloc] init];
125+
[package.events
126+
enumerateObjectsUsingBlock:^(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) {
127+
NSMutableSet *logSet = logMappingIDToLogSet[event.mappingID];
128+
logSet = logSet ? logSet : [[NSMutableSet alloc] init];
129+
[logSet addObject:event];
130+
logMappingIDToLogSet[event.mappingID] = logSet;
131+
}];
132+
133+
gdt_cct_BatchedLogRequest batchedLogRequest =
134+
GDTCCTConstructBatchedLogRequest(logMappingIDToLogSet);
135+
136+
NSData *data = GDTCCTEncodeBatchedLogRequest(&batchedLogRequest);
137+
pb_release(gdt_cct_BatchedLogRequest_fields, &batchedLogRequest);
138+
return data ? data : [[NSData alloc] init];
39139
}
40140

41141
@end

GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTPrioritizer.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#import <GoogleDataTransport/GoogleDataTransport.h>
2020

21+
NS_ASSUME_NONNULL_BEGIN
22+
2123
/** Manages the prioritization of events from GoogleDataTransport. */
2224
@interface GDTCCTPrioritizer : NSObject <GDTPrioritizer>
2325

@@ -30,7 +32,12 @@
3032
/** The most recent attempted upload of daily uploaded logs. */
3133
@property(nonatomic) GDTClock *timeOfLastDailyUpload;
3234

33-
/** Creates and/or returns the singleton instance of the prioritizer. */
35+
/** Creates and/or returns the singleton instance of this class.
36+
*
37+
* @return The singleton instance of this class.
38+
*/
3439
+ (instancetype)sharedInstance;
3540

41+
NS_ASSUME_NONNULL_END
42+
3643
@end

GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport/Classes/Private/GDTCCTUploader.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,29 @@
1818

1919
#import <GoogleDataTransport/GoogleDataTransport.h>
2020

21+
NS_ASSUME_NONNULL_BEGIN
22+
2123
/** Class capable of uploading events to the CCT backend. */
2224
@interface GDTCCTUploader : NSObject <GDTUploader>
2325

24-
/** Creates/returns the single instance. */
26+
/** The queue on which all CCT uploading will occur. */
27+
@property(nonatomic, readonly) dispatch_queue_t uploaderQueue;
28+
29+
/** The server URL to upload to. Look at .m for the default value. */
30+
@property(nonatomic) NSURL *serverURL;
31+
32+
/** The URL session that will attempt upload. */
33+
@property(nonatomic, readonly) NSURLSession *uploaderSession;
34+
35+
/** The current upload task. */
36+
@property(nullable, nonatomic, readonly) NSURLSessionUploadTask *currentTask;
37+
38+
/** Creates and/or returns the singleton instance of this class.
39+
*
40+
* @return The singleton instance of this class.
41+
*/
2542
+ (instancetype)sharedInstance;
2643

2744
@end
45+
46+
NS_ASSUME_NONNULL_END
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <XCTest/XCTest.h>
18+
19+
#import <GoogleDataTransport/GoogleDataTransport.h>
20+
#import <SystemConfiguration/SCNetworkReachability.h>
21+
22+
#import "GDTCCTPrioritizer.h"
23+
#import "GDTCCTUploader.h"
24+
25+
typedef void (^GDTCCTIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable);
26+
27+
@interface GDTCCTTestDataObject : NSObject <GDTEventDataObject>
28+
29+
@end
30+
31+
@implementation GDTCCTTestDataObject
32+
33+
- (NSData *)transportBytes {
34+
// Return some random event data corresponding to mapping ID 1018.
35+
NSBundle *testBundle = [NSBundle bundleForClass:[self class]];
36+
NSArray *dataFiles = @[
37+
@"message-32347456.dat", @"message-35458880.dat", @"message-39882816.dat",
38+
@"message-40043840.dat", @"message-40657984.dat"
39+
];
40+
NSURL *fileURL = [testBundle URLForResource:dataFiles[arc4random_uniform(5)] withExtension:nil];
41+
return [NSData dataWithContentsOfURL:fileURL];
42+
}
43+
44+
@end
45+
46+
@interface GDTCCTIntegrationTest : XCTestCase
47+
48+
/** If YES, the network conditions were good enough to allow running integration tests. */
49+
@property(nonatomic) BOOL okToRunTest;
50+
51+
/** If YES, allow the recursive generating of events. */
52+
@property(nonatomic) BOOL generateEvents;
53+
54+
/** The transporter used by the test. */
55+
@property(nonatomic) GDTTransport *transport;
56+
57+
@end
58+
59+
@implementation GDTCCTIntegrationTest
60+
61+
- (void)setUp {
62+
self.generateEvents = YES;
63+
SCNetworkReachabilityRef reachabilityRef =
64+
SCNetworkReachabilityCreateWithName(CFAllocatorGetDefault(), "https://google.com");
65+
SCNetworkReachabilityFlags flags;
66+
Boolean success = SCNetworkReachabilityGetFlags(reachabilityRef, &flags);
67+
if (success) {
68+
self.okToRunTest =
69+
(flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable;
70+
self.transport = [[GDTTransport alloc] initWithMappingID:@"1018"
71+
transformers:nil
72+
target:kGDTTargetCCT];
73+
}
74+
}
75+
76+
/** Generates an event and sends it through the transport infrastructure. */
77+
- (void)generateEvent {
78+
GDTEvent *event = [self.transport eventForTransport];
79+
event.dataObject = [[GDTCCTTestDataObject alloc] init];
80+
[self.transport sendDataEvent:event];
81+
}
82+
83+
/** Generates events recursively at random intervals between 0 and 5 seconds. */
84+
- (void)recursivelyGenerateEvent {
85+
if (self.generateEvents) {
86+
GDTEvent *event = [self.transport eventForTransport];
87+
event.dataObject = [[GDTCCTTestDataObject alloc] init];
88+
[self.transport sendDataEvent:event];
89+
dispatch_after(
90+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random_uniform(6) * NSEC_PER_SEC)),
91+
dispatch_get_main_queue(), ^{
92+
[self recursivelyGenerateEvent];
93+
});
94+
}
95+
}
96+
97+
/** Tests sending data to CCT with a high priority event if network conditions are good. */
98+
- (void)testSendingDataToCCT {
99+
if (!self.okToRunTest) {
100+
NSLog(@"Skipping the integration test, as the network conditions weren't good enough.");
101+
return;
102+
}
103+
104+
NSUInteger lengthOfTestToRunInSeconds = 10;
105+
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
106+
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
107+
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
108+
dispatch_source_set_event_handler(timer, ^{
109+
static int numberOfTimesCalled = 0;
110+
numberOfTimesCalled++;
111+
if (numberOfTimesCalled < lengthOfTestToRunInSeconds) {
112+
[self generateEvent];
113+
} else {
114+
dispatch_source_cancel(timer);
115+
}
116+
});
117+
dispatch_resume(timer);
118+
119+
// Run for a bit, several seconds longer than the previous bit.
120+
[[NSRunLoop currentRunLoop]
121+
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:lengthOfTestToRunInSeconds + 5]];
122+
123+
XCTestExpectation *taskCreatedExpectation = [self expectationWithDescription:@"task created"];
124+
XCTestExpectation *taskDoneExpectation = [self expectationWithDescription:@"task done"];
125+
126+
[[GDTCCTUploader sharedInstance]
127+
addObserver:self
128+
forKeyPath:@"currentTask"
129+
options:NSKeyValueObservingOptionNew
130+
context:(__bridge void *_Nullable)(^(NSURLSessionUploadTask *_Nullable task) {
131+
if (task) {
132+
[taskCreatedExpectation fulfill];
133+
} else {
134+
[taskDoneExpectation fulfill];
135+
}
136+
})];
137+
138+
// Send a high priority event to flush events.
139+
GDTEvent *event = [self.transport eventForTransport];
140+
event.dataObject = [[GDTCCTTestDataObject alloc] init];
141+
event.qosTier = GDTEventQoSFast;
142+
[self.transport sendDataEvent:event];
143+
144+
[self waitForExpectations:@[ taskCreatedExpectation, taskDoneExpectation ] timeout:25.0];
145+
146+
// Just run for a minute whilst generating events.
147+
NSInteger secondsToRun = 65;
148+
[self generateEvents];
149+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsToRun * NSEC_PER_SEC)),
150+
dispatch_get_main_queue(), ^{
151+
self.generateEvents = NO;
152+
});
153+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:secondsToRun]];
154+
}
155+
156+
// KVO is utilized here to know whether or not the task has completed.
157+
- (void)observeValueForKeyPath:(NSString *)keyPath
158+
ofObject:(id)object
159+
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
160+
context:(void *)context {
161+
if ([keyPath isEqualToString:@"currentTask"]) {
162+
NSURLSessionUploadTask *task = change[NSKeyValueChangeNewKey];
163+
typedef void (^GDTCCTIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable);
164+
if (context) {
165+
GDTCCTIntegrationTestBlock block = (__bridge GDTCCTIntegrationTestBlock)context;
166+
block([task isKindOfClass:[NSNull class]] ? nil : task);
167+
}
168+
}
169+
}
170+
171+
@end

0 commit comments

Comments
 (0)