-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
## Задача | ||
|
||
Есть некий сервис, который отдает нам поток биржевых данных. Мы хотели бы раздавать этот поток другим внутренним клиентам, заодно агрегируя данные в формат, достаточный для отрисовки [candlestick chart](https://en.wikipedia.org/wiki/Candlestick_chart). Нужно реализовать сервер для этого на Scala или Java. Желательно использование фреймворков, облегчающих написание сетевых серверов (Netty, Mina, Akka IO). | ||
|
||
Для тестирования можно использовать тестовый upstream.py, запустив его при помощи команды `python upstream.py`. После запуска он будет ждать соединений на `127.0.0.1:5555` и генерировать поток случайных сделок. | ||
|
||
## Протокол (сервер/upstream) | ||
|
||
Наш сервер соединяется к провайдеру данных по протоколу TCP/IP, после чего тот начинает присылать биржевые сделки в виде сообщений следующего формата: | ||
|
||
``` | ||
[ LEN:2 ] [ TIMESTAMP:8 ] [ TICKER_LEN:2 ] [ TICKER:TICKER_LEN ] [ PRICE:8 ] [ SIZE:4 ] | ||
``` | ||
|
||
где поля имеют следующую семантику: | ||
|
||
* `LEN`: длина последующего сообщения (целое, 2 байта) | ||
* `TIMESTAMP`: дата и время события (целое, 8 байт, milliseconds since epoch) | ||
* `TICKER_LEN`: длина биржевого тикера (целое, 2 байта) | ||
* `TICKER`: биржевой тикер (ASCII, TICKER_LEN байт) | ||
* `PRICE`: цена сделки (double, 8 байт) | ||
* `SIZE`: объем сделки (целое, 4 байта) | ||
|
||
## Протокол (клиент/сервер) | ||
|
||
Клиенты подсоединяются к серверу также по TCP/IP. При подключении нового клиента сервер посылает ему минутные свечи за последние 10 минут, после чего в начале каждой новой минуты присылает свечи за предыдущую минуту. Данные сериализуются в JSON-сообщения следующего вида, разделенные символом перевода строки (\n): | ||
|
||
``` | ||
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:02:00Z", "open": 112.1, "high": 115.2, "low": 110.0, "close": 114.2, "volume": 13000 } | ||
``` | ||
|
||
## Пример | ||
|
||
Сообщения c upstream (в человекочитаемом формате): | ||
|
||
``` | ||
2016-01-01 15:02:10 AAPL 101.1 200 | ||
2016-01-01 15:02:15 AAPL 101.2 100 | ||
2016-01-01 15:02:25 AAPL 101.3 300 | ||
2016-01-01 15:02:35 MSFT 120.1 500 | ||
2016-01-01 15:02:40 AAPL 101.0 700 | ||
2016-01-01 15:03:10 AAPL 102.1 1000 | ||
2016-01-01 15:03:11 MSFT 120.2 1000 | ||
2016-01-01 15:03:30 AAPL 103.2 100 | ||
2016-01-01 15:03:31 MSFT 120.0 700 | ||
2016-01-01 15:04:21 AAPL 102.1 100 | ||
2016-01-01 15:04:21 MSFT 102.1 200 | ||
``` | ||
|
||
Клиент подсоединяется в `15:04:04`, получает свечи за последние 10 минут: | ||
|
||
``` | ||
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:02:00Z", "open": 101.1, "high": 101.3, "low": 101, "close": 101, "volume": 1300 } | ||
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:02:00Z", "open": 120.1, "high": 120.1, "low": 120.1, "close": 120.1, "volume": 500 } | ||
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:03:00Z", "open": 102.1, "high": 103.2, "low": 102.1, "close": 103.2, "volume": 1100 } | ||
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:03:00Z", "open": 120.2, "high": 120.2, "low": 120, "close": 120, "volume": 1700 } | ||
``` | ||
|
||
В `15:05:00` клиент получает свечи за предыдущую минуту: | ||
|
||
``` | ||
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:04:00Z", "open": 102.1, "high": 102.1, "low": 102.1, "close": 102.1, "volume": 100 } | ||
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:04:00Z", "open": 120.1, "high": 120.1, "low": 120.1, "close": 120.1, "volume": 200 } | ||
``` | ||
|
||
## Upstream | ||
|
||
```python | ||
from __future__ import print_function | ||
|
||
import threading | ||
import socket | ||
import struct | ||
import time | ||
from datetime import datetime | ||
import random | ||
|
||
TICKERS = ["AAPL", "GOOG", "MSFT", "SPY"] | ||
PORT = 5555 | ||
|
||
def timestamp_millis(timestamp): | ||
return int((timestamp - datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) | ||
|
||
def send_trade(client_socket, host, port): | ||
timestamp = datetime.utcnow() | ||
ticker = random.choice(TICKERS) | ||
price = 90 + random.randrange(0, 400) * 0.05 | ||
size = random.randrange(100, 10000, 100) | ||
msg = struct.pack("!QH%dsdI" % len(ticker), timestamp_millis(timestamp), len(ticker), ticker.encode("ascii"), price, size) | ||
msg_len = struct.pack("!H", len(msg)) | ||
print("[%s:%d] %s: %s %.2f %d" % (host, port, timestamp, ticker, price, size)) | ||
client_socket.send(msg_len + msg) | ||
|
||
def emit_trades(client_socket, addr): | ||
(host, port) = addr | ||
try: | ||
while True: | ||
time.sleep(random.uniform(0.2, 3)) | ||
send_trade(client_socket, host, port) | ||
except Exception as e: | ||
print("[%s:%d] Error: %s" % (host, port, e)) | ||
finally: | ||
client_socket.close() | ||
|
||
def main(): | ||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
server.bind(("127.0.0.1", PORT)) | ||
server.listen(5) | ||
print("Waiting for connections on port %d" % PORT) | ||
while True: | ||
(client, addr) = server.accept() | ||
print("Incoming connection from %s:%d" % addr) | ||
client_thread = threading.Thread(target=emit_trades, args=(client, addr)) | ||
client_thread.daemon = True | ||
client_thread.start() | ||
|
||
if __name__ == "__main__": | ||
main() | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
## The task goal | ||
Execution of "gradle build" have to be successfully completed (passing test phase also). | ||
|
||
### Description | ||
A user submits a search query to find an available domain name. | ||
|
||
The system should check domain availability with in all supported TLD zones (.com/.net/.club) and return the results among with the registration price for each zone. | ||
|
||
A domain is available for registration if it cannot be found in `domains` database table. | ||
|
||
Prices for each zone are stored in `tlds` database table. | ||
|
||
Request format: `/domains/check?search=new-domain` | ||
|
||
Response format: | ||
``` | ||
[ | ||
{"domain": "new-domain.com", tld: "com", "available": false, "price": 8.99}, | ||
{"domain": "new-domain.net", tld: "net", "available": true, "price": 9.99}, | ||
{"domain": "new-domain.club", tld: "club", "available": true, "price": 15.99} | ||
] | ||
``` | ||
Response items have to be sorted by price ascending (tld with low price at first) | ||
|
||
. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
plugins { | ||
id 'org.springframework.boot' version '2.1.3.RELEASE' | ||
id 'io.spring.dependency-management' version '1.0.8.RELEASE' | ||
id 'java' | ||
} | ||
|
||
group = 'com.test' | ||
version = '0.0.1-SNAPSHOT' | ||
sourceCompatibility = '1.8' | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
implementation 'org.springframework.boot:spring-boot-starter-web' | ||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
implementation 'com.h2database:h2' | ||
implementation 'org.flywaydb:flyway-core' | ||
compile 'org.projectlombok:lombok' | ||
|
||
testCompile 'com.jayway.jsonpath:json-path' | ||
testImplementation('org.springframework.boot:spring-boot-starter-test') { | ||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' | ||
} | ||
testImplementation 'org.junit.jupiter:junit-jupiter-api' | ||
testImplementation 'org.junit.jupiter:junit-jupiter-params' | ||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rootProject.name = 'rest-service' |
16 changes: 16 additions & 0 deletions
16
testrestapi/src/main/java/com/test/restservice/DomainController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.test.restservice; | ||
|
||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
public class DomainController { | ||
|
||
|
||
@GetMapping("/domains/check") | ||
public Object check(SearchDto searchDto) { | ||
// todo start point for implementation | ||
return null; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
testrestapi/src/main/java/com/test/restservice/RestServiceApplication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.test.restservice; | ||
|
||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; | ||
|
||
@SpringBootApplication(scanBasePackages = {"com.test"}) | ||
@EnableJpaRepositories | ||
public class RestServiceApplication { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(RestServiceApplication.class, args); | ||
} | ||
|
||
} |
8 changes: 8 additions & 0 deletions
8
testrestapi/src/main/java/com/test/restservice/SearchDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.test.restservice; | ||
|
||
import lombok.Data; | ||
|
||
@Data | ||
public class SearchDto { | ||
private String search; | ||
} |
24 changes: 24 additions & 0 deletions
24
testrestapi/src/main/java/com/test/restservice/model/Domain.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.test.restservice.model; | ||
|
||
import lombok.EqualsAndHashCode; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
|
||
import javax.persistence.*; | ||
import java.io.Serializable; | ||
|
||
@Getter | ||
@Setter | ||
@NoArgsConstructor | ||
@EqualsAndHashCode(callSuper = false) | ||
@Entity | ||
@Table(name = "domains") | ||
public class Domain implements Serializable { | ||
@Id | ||
@Column | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
@Column | ||
private String name; | ||
} |
24 changes: 24 additions & 0 deletions
24
testrestapi/src/main/java/com/test/restservice/model/Tld.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.test.restservice.model; | ||
|
||
import lombok.*; | ||
|
||
import javax.persistence.*; | ||
import java.io.Serializable; | ||
|
||
@Getter | ||
@Setter | ||
@Entity | ||
@EqualsAndHashCode(callSuper = false) | ||
@Table(name = "tlds") | ||
public class Tld implements Serializable { | ||
@Id | ||
@Column | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
@Column | ||
private String name; | ||
@Column | ||
private Double price; | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
spring: | ||
datasource: | ||
url: jdbc:h2:mem:testdb | ||
driver-class-name: org.h2.Driver | ||
username: sa | ||
password: password | ||
|
||
jpa: | ||
database: h2 | ||
generate-ddl: false | ||
hibernate.ddl-auto: none | ||
open-in-view: false | ||
database-platform: org.hibernate.dialect.H2Dialect | ||
show-sql: true | ||
|
||
flyway: | ||
enabled: true | ||
locations: classpath:db | ||
validate-on-migrate: false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
CREATE TABLE tlds ( | ||
id INT AUTO_INCREMENT PRIMARY KEY, | ||
name VARCHAR(16) NOT NULL, | ||
price DOUBLE NOT NULL | ||
|
||
); | ||
|
||
INSERT INTO tlds (name, price) VALUES | ||
('com', 8.99), | ||
('club', 15.99), | ||
('net', 9.99); | ||
|
||
CREATE TABLE domains ( | ||
id INT AUTO_INCREMENT PRIMARY KEY, | ||
name VARCHAR(255) NOT NULL | ||
); | ||
|
||
INSERT INTO domains (name) VALUES | ||
('existing'); |
67 changes: 67 additions & 0 deletions
67
testrestapi/src/test/java/com/test/restservice/DomainControllerTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright 2016 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package com.test.restservice; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.test.web.servlet.MockMvc; | ||
|
||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
||
@SpringBootTest | ||
@AutoConfigureMockMvc | ||
public class DomainControllerTests { | ||
|
||
@Autowired | ||
private MockMvc mockMvc; | ||
|
||
@Test | ||
public void testExisting() throws Exception { | ||
|
||
this.mockMvc.perform(get("/domains/check?search=existing")) | ||
.andDo(print()).andExpect(status().isOk()) | ||
.andExpect(jsonPath("$[0].tld").value("com")) | ||
.andExpect(jsonPath("$[0].price").value(8.99)) | ||
.andExpect(jsonPath("$[0].available").value(false)) | ||
.andExpect(jsonPath("$[1].tld").value("net")) | ||
.andExpect(jsonPath("$[1].price").value(9.99)) | ||
.andExpect(jsonPath("$[1].available").value(true)) | ||
.andExpect(jsonPath("$[2].tld").value("club")) | ||
.andExpect(jsonPath("$[2].price").value(15.99)) | ||
.andExpect(jsonPath("$[2].available").value(true)); | ||
} | ||
|
||
@Test | ||
public void testNotExisting() throws Exception { | ||
|
||
this.mockMvc.perform(get("/domains/check?search=new")) | ||
.andDo(print()).andExpect(status().isOk()) | ||
.andExpect(jsonPath("$[0].tld").value("com")) | ||
.andExpect(jsonPath("$[0].price").value(8.99)) | ||
.andExpect(jsonPath("$[0].available").value(true)) | ||
.andExpect(jsonPath("$[1].tld").value("net")) | ||
.andExpect(jsonPath("$[1].price").value(9.99)) | ||
.andExpect(jsonPath("$[1].available").value(true)) | ||
.andExpect(jsonPath("$[2].tld").value("club")) | ||
.andExpect(jsonPath("$[2].price").value(15.99)) | ||
.andExpect(jsonPath("$[2].available").value(true)); | ||
} | ||
} |