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

control-service: java 17 #1439

Merged
merged 72 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
77133ce
control-service: java 17
murphp15 Dec 13, 2022
0ae817a
Merge branch 'main' into person/murphp15/java_17
murphp15 Dec 14, 2022
6025509
java 17
murphp15 Dec 14, 2022
74c59e8
java 17
murphp15 Dec 14, 2022
ce485b0
java 17
murphp15 Dec 14, 2022
366ef7a
java 17
murphp15 Dec 14, 2022
019858a
Merge branch 'main' into person/murphp15/java_17
murphp15 Dec 14, 2022
6276e8a
Merge branch 'main' into person/murphp15/java_17
murphp15 Dec 19, 2022
4a67d53
java 17
murphp15 Dec 19, 2022
8456f7d
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 16, 2023
78c2b2f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
0559a33
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
61f11d0
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
5d4d663
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
b059ae9
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
3aea47b
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
4163050
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
3f20905
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
d39a0e1
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 18, 2023
dc1d197
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 19, 2023
5015821
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 19, 2023
b88f26f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 19, 2023
d8d037f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
abf9eb8
Google Java Format
Jan 21, 2023
0c3e0de
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
a434efd
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
eed7b49
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
c776e34
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
fe1eec9
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
b1b4263
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
fd092c0
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
7dfa7be
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
f4b4eec
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
ed0b09f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
97dcd90
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 21, 2023
c10ead8
Google Java Format
Jan 21, 2023
a922984
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 22, 2023
9234ece
Google Java Format
Jan 22, 2023
aadb422
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 22, 2023
f636c88
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 22, 2023
8d62527
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
913c9dd
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
2dc2b3b
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
871ff44
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
5a3c57b
Google Java Format
Jan 23, 2023
842993f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
17b2e65
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
2aa3eb2
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
8f1957b
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 23, 2023
e60c8e9
control-service: use latest docker image
murphp15 Jan 23, 2023
3adbd69
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 24, 2023
d1e5b57
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 24, 2023
76bbf51
Update format.yml
murphp15 Jan 24, 2023
dc84793
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 25, 2023
45eb69f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 25, 2023
5a61414
control-service: only delete file in test path
murphp15 Jan 25, 2023
a907678
control-service: only delete file in test path
murphp15 Jan 25, 2023
d92fa22
Google Java Format
Jan 25, 2023
59eb93c
control-service: only delete file in test path
murphp15 Jan 25, 2023
c71e10b
control-service: only delete file in test path
murphp15 Jan 25, 2023
00d134b
Google Java Format
Jan 25, 2023
51662f9
control-service: only delete file in test path
murphp15 Jan 25, 2023
aa55f2d
Google Java Format
Jan 25, 2023
2681232
control-service: only delete file in test path
murphp15 Jan 25, 2023
3088b50
Google Java Format
Jan 25, 2023
0763585
control-service: fix release
murphp15 Jan 25, 2023
f7aed78
control-service: fix release
murphp15 Jan 25, 2023
dafc28f
control-service: only delete file in test path
murphp15 Jan 25, 2023
7f48c6a
Google Java Format
Jan 25, 2023
6c3ae0f
Merge branch 'main' into person/murphp15/java_17
murphp15 Jan 25, 2023
36c2a03
control-service: fix release
murphp15 Jan 25, 2023
e8c96ae
control-service: fix release
murphp15 Jan 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 12 additions & 20 deletions projects/control-service/cicd/.gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
variables:
_JAVA_OPTIONS: "-Xms2048m -Xmx4096m"
before_script:
- apk --no-cache add openjdk11-jdk git zip curl zip py-pip
- apk --no-cache add openjdk17-jdk git zip curl zip py-pip
- pip install --upgrade pip && pip install awscli

control_service_build_image:
Expand All @@ -42,7 +42,7 @@ control_service_build_image:
- .images:dind
stage: build
script:
- apk --no-cache add git openjdk11-jdk
- apk --no-cache add git openjdk17-jdk
- export TAG=$(git rev-parse --short HEAD)
- cd projects/control-service/projects
- ./gradlew -p ./model build publishToMavenLocal --info --stacktrace
Expand Down Expand Up @@ -197,7 +197,7 @@ control_service_publish_image:
extends: .images:dind
stage: publish_artifacts
script:
- apk add --no-cache git openjdk11-jdk
- apk add --no-cache git openjdk17-jdk
- export TAG=$(git rev-parse --short HEAD)
- docker login -u $VDK_DOCKER_REGISTRY_USERNAME -p $VDK_DOCKER_REGISTRY_PASSWORD
- cd projects/control-service/projects
Expand All @@ -222,7 +222,7 @@ control_service_publish_api_client:
image: docker:20.10.22
stage: publish_artifacts
script:
- apk add --no-cache py-pip openjdk11-jdk git python
- apk add --no-cache py-pip openjdk17-jdk git python
- pip install -U pip setuptools wheel twine
- ./projects/control-service/projects/gradlew -p ./projects/control-service/projects/model buildPython
- cd projects/control-service/projects/model/apidefs/datajob-api/build
Expand Down Expand Up @@ -252,14 +252,10 @@ control_service_deploy_testing_data_pipelines:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
- bash -ex ./projects/control-service/cicd/deploy-testing-pipelines-service.sh
retry: !reference [.control_service_retry, retry_options]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes: [ projects/control-service/projects/helm_charts/pipelines-control-service/version.txt ]
when: never
- if: '$CI_COMMIT_BRANCH == "main"'
changes: *control_service_code_change_locations
- if: '$CI_COMMIT_BRANCH == "main"'
changes: *control_service_helm_change_locations
only:
refs:
# TODO revert before merging
- external_pull_requests

# vdk-heartbeat source: https://github.com/vmware/versatile-data-kit/tree/main/projects/vdk-heartbeat
control_service_post_deployment_test:
Expand All @@ -274,14 +270,10 @@ control_service_post_deployment_test:
- export VDK_HEARTBEAT_OP_ID="vdkcs-$CI_PIPELINE_ID"
- vdk-heartbeat -f projects/control-service/cicd/post_deploy_test_heartbeat_config.ini
retry: !reference [.control_service_retry, retry_options]
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes: [ projects/control-service/projects/helm_charts/pipelines-control-service/version.txt ]
when: never
- if: '$CI_COMMIT_BRANCH == "main"'
changes: *control_service_code_change_locations
- if: '$CI_COMMIT_BRANCH == "main"'
changes: *control_service_helm_change_locations
only:
refs:
# TODO revert before merging
- external_pull_requests

control_service_release:
image: docker:20.10.22
Expand Down
4 changes: 2 additions & 2 deletions projects/control-service/projects/java-common.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
apply plugin: 'java'
apply plugin: 'idea'
sourceCompatibility = '11'
targetCompatibility = '11'
sourceCompatibility = '17'
targetCompatibility = '17'
group 'com.vmware.taurus'
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
# Builder stage
# This stage is implemented like in the guide, but it may be more efficient to
# have it in the gradle build process outside docker.
FROM adoptopenjdk:11-jre-hotspot as builder
FROM eclipse-temurin:17 as builder
ARG JAR_NAME
WORKDIR application
COPY ./${JAR_NAME} application.jar
RUN java -Djarmode=layertools -jar application.jar extract


# Final stage
FROM adoptopenjdk:11-jre-hotspot
FROM eclipse-temurin:17
LABEL maintainer="Project Versatile Data Kit <[email protected]>"

# install kerberos client so that we can use kadmin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package com.vmware.taurus.datajobs.it.common;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;

import com.vmware.taurus.service.JobExecutionRepository;
import com.vmware.taurus.service.model.DataJob;
Expand All @@ -15,6 +16,16 @@

public class JobExecutionUtil {

/**
* at the database level we only store date-times accurate to the microsecond. Like wise in older
* versions of java .now() returned timestamps accurate to micro-seconds. In newer versions of
* java .now() gives nano-second precision and it causes tests written before we adopted that java
* version to fail.
*/
public static OffsetDateTime getTimeAccurateToMicroSecond() {
return OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS);
}

public static DataJobExecution createDataJobExecution(
JobExecutionRepository jobExecutionRepository,
String executionId,
Expand All @@ -37,7 +48,7 @@ public static DataJobExecution createDataJobExecution(
.resourcesMemoryLimit(1000)
.message("message")
.lastDeployedBy("test_user")
.lastDeployedDate(OffsetDateTime.now())
.lastDeployedDate(getTimeAccurateToMicroSecond())
.jobVersion("test_version")
.jobSchedule("*/5 * * * *")
.opId("test_op_id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.time.OffsetDateTime;
import java.util.List;

import static com.vmware.taurus.datajobs.it.common.JobExecutionUtil.getTimeAccurateToMicroSecond;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand Down Expand Up @@ -67,7 +68,7 @@ public void setup() {
this.dataJob2 = jobsRepository.save(new DataJob(TEST_JOB_NAME_2, config2));
this.dataJob3 = jobsRepository.save(new DataJob(TEST_JOB_NAME_3, config3));

OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime now = getTimeAccurateToMicroSecond();
this.dataJobExecution1 =
JobExecutionUtil.createDataJobExecution(
jobExecutionRepository, "testId1", dataJob1, now, now, ExecutionStatus.SUCCEEDED);
Expand Down Expand Up @@ -101,9 +102,8 @@ private static String getQuery() {
}

private void cleanup() {
jobsRepository
.findAllById(List.of(TEST_JOB_NAME_1, TEST_JOB_NAME_2, TEST_JOB_NAME_3))
.forEach(dataJob -> jobsRepository.delete(dataJob));
jobsRepository.deleteAll(
jobsRepository.findAllById(List.of(TEST_JOB_NAME_1, TEST_JOB_NAME_2, TEST_JOB_NAME_3)));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import java.text.MessageFormat;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;

import org.hamcrest.Matchers;
Expand Down Expand Up @@ -132,7 +133,7 @@ public void testExecutions_filterByStartTimeGte() throws Exception {
"{"
+ "\"filter\": {"
+ " \"startTimeGte\": \""
+ dataJobExecution3.getStartTime()
+ dataJobExecution3.getStartTime().truncatedTo(ChronoUnit.MICROS)
+ "\""
+ " },"
+ "\"pageNumber\": 1,"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.UUID;

import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -95,7 +96,7 @@ public void testCallWithSingleExecution(String jobName, String username) throws
.andExpect(jsonPath("$.data.content[0].deployments[0].executions[0].id").value(expectedId))
.andExpect(
jsonPath("$.data.content[0].deployments[0].executions[0].endTime")
.value(expectedEndTime.toString()));
.value(roundDateTimeToMicros(expectedEndTime)));
}

@Test
Expand All @@ -120,11 +121,11 @@ public void testCallTwoExecutionsSortAsc(String jobName, String username) throws
.andExpect(jsonPath("$.data.content[0].deployments[0].executions[0].id").value(expectedId2))
.andExpect(
jsonPath("$.data.content[0].deployments[0].executions[0].endTime")
.value(expectedEndTimeSmaller.toString()))
.value(roundDateTimeToMicros(expectedEndTimeSmaller)))
.andExpect(jsonPath("$.data.content[0].deployments[0].executions[1].id").value(expectedId1))
.andExpect(
jsonPath("$.data.content[0].deployments[0].executions[1].endTime")
.value(expectedEndTimeLarger.toString()));
.value(roundDateTimeToMicros(expectedEndTimeLarger)));
}

@Test
Expand All @@ -149,11 +150,11 @@ public void testCallTwoExecutionsSortDesc(String jobName, String username) throw
.andExpect(jsonPath("$.data.content[0].deployments[0].executions[0].id").value(expectedId1))
.andExpect(
jsonPath("$.data.content[0].deployments[0].executions[0].endTime")
.value(expectedEndTimeLarger.toString()))
.value(roundDateTimeToMicros(expectedEndTimeLarger)))
.andExpect(jsonPath("$.data.content[0].deployments[0].executions[1].id").value(expectedId2))
.andExpect(
jsonPath("$.data.content[0].deployments[0].executions[1].endTime")
.value(expectedEndTimeSmaller.toString()));
.value(roundDateTimeToMicros(expectedEndTimeSmaller)));
}

@Test
Expand Down Expand Up @@ -200,4 +201,8 @@ private void createDataJobExecution(String executionId, String jobName, OffsetDa

jobExecutionRepository.save(jobExecution);
}

private static String roundDateTimeToMicros(OffsetDateTime dateTime) {
return dateTime.plusNanos(500).truncatedTo(ChronoUnit.MICROS).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.junit.jupiter.api.Assertions;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;

public final class RepositoryUtil {

Expand Down Expand Up @@ -82,7 +83,7 @@ public static DataJobExecution createDataJobExecution(
executionStatus,
message,
startTime,
OffsetDateTime.now());
getTimeAccurateToMicroSecond());
}

public static DataJobExecution createDataJobExecution(
Expand All @@ -99,7 +100,7 @@ public static DataJobExecution createDataJobExecution(
executionStatus,
"test_message",
startTime,
OffsetDateTime.now());
getTimeAccurateToMicroSecond());
}

public static DataJobExecution createDataJobExecution(
Expand All @@ -115,8 +116,8 @@ public static DataJobExecution createDataJobExecution(
dataJob,
executionStatus,
message,
OffsetDateTime.now(),
OffsetDateTime.now());
getTimeAccurateToMicroSecond(),
getTimeAccurateToMicroSecond());
}

public static DataJobExecution createDataJobExecution(
Expand All @@ -142,7 +143,7 @@ public static DataJobExecution createDataJobExecution(
.resourcesMemoryLimit(1000)
.message(message)
.lastDeployedBy("test_user")
.lastDeployedDate(OffsetDateTime.now())
.lastDeployedDate(getTimeAccurateToMicroSecond())
.jobVersion("test_version")
.jobSchedule("*/5 * * * *")
.opId("test_op_id")
Expand All @@ -151,4 +152,14 @@ public static DataJobExecution createDataJobExecution(

return jobExecutionRepository.save(expectedJobExecution);
}

/**
* at the database level we only store date-times accurate to the microsecond. Like wise in older
* versions of java .now() returned timestamps accurate to micro-seconds. In newer versions of
* java .now() gives nano-second precision and it causes tests written before we adopted that java
* version to fail.
*/
public static OffsetDateTime getTimeAccurateToMicroSecond() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the documentation great. My only concern is with duplicated methods
with JobExecutionUtil

return OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.time.OffsetDateTime;
import java.util.List;

import static com.vmware.taurus.RepositoryUtil.getTimeAccurateToMicroSecond;

@SpringBootTest(classes = ControlplaneApplication.class)
public class JobExecutionFilterSpecIT {

Expand Down Expand Up @@ -69,7 +71,7 @@ public void testJobExecutionFilterSpec_filerByStatusIn_shouldReturnResult() {
@Test
public void testJobExecutionFilterSpec_filerByStartTimeGte_shouldReturnResult() {
DataJob actualDataJob = RepositoryUtil.createDataJob(jobsRepository);
OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime now = getTimeAccurateToMicroSecond();

RepositoryUtil.createDataJobExecution(
jobExecutionRepository,
Expand Down Expand Up @@ -112,7 +114,7 @@ public void testJobExecutionFilterSpec_filerByStartTimeGte_shouldReturnResult()
@Test
public void testJobExecutionFilterSpec_filerByEndTimeGte_shouldReturnResult() {
DataJob actualDataJob = RepositoryUtil.createDataJob(jobsRepository);
OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime now = getTimeAccurateToMicroSecond();

RepositoryUtil.createDataJobExecution(
jobExecutionRepository,
Expand Down Expand Up @@ -159,7 +161,7 @@ public void testJobExecutionFilterSpec_filerByEndTimeGte_shouldReturnResult() {
@Test
public void testJobExecutionFilterSpec_filerByAllFields_shouldReturnResult() {
DataJob actualDataJob = RepositoryUtil.createDataJob(jobsRepository);
OffsetDateTime now = OffsetDateTime.now();
OffsetDateTime now = getTimeAccurateToMicroSecond();

RepositoryUtil.createDataJobExecution(
jobExecutionRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.time.ZoneOffset;
import java.util.List;

import static com.vmware.taurus.RepositoryUtil.getTimeAccurateToMicroSecond;

/** Integration tests of the setup of Spring Data repository for data job executions */
@SpringBootTest(classes = ControlplaneApplication.class)
public class JobExecutionRepositoryIT {
Expand Down Expand Up @@ -246,7 +248,7 @@ void testUpdateExecutionStatusWhereOldStatusInAndExecutionIdIn_withExecutions_sh
jobExecutionRepository.save(execution1);
jobExecutionRepository.save(execution2);

var executionEndTime = OffsetDateTime.now();
var executionEndTime = getTimeAccurateToMicroSecond();
var message = "Changed by test";
jobExecutionRepository.updateExecutionStatusWhereOldStatusInAndExecutionIdIn(
ExecutionStatus.CANCELLED,
Expand Down Expand Up @@ -293,7 +295,7 @@ void testUpdateExecutionStatusWhereOldStatusInAndExecutionIdIn_withExecutions_sh
jobExecutionRepository.save(execution1);
jobExecutionRepository.save(execution2);

var executionEndTime = OffsetDateTime.now();
var executionEndTime = getTimeAccurateToMicroSecond();
var message = "Changed by test";
jobExecutionRepository.updateExecutionStatusWhereOldStatusInAndExecutionIdIn(
ExecutionStatus.CANCELLED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import com.vmware.taurus.service.model.ExecutionStatus;
import com.vmware.taurus.service.model.ExecutionResult;

import static com.vmware.taurus.RepositoryUtil.getTimeAccurateToMicroSecond;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = ControlplaneApplication.class)
public class JobExecutionServiceUpdateExecutionIT {
Expand Down Expand Up @@ -233,7 +235,7 @@ private KubernetesService.JobExecution createJobExecution(
.resourcesMemoryLimit(1)
.resourcesMemoryRequest(1)
.deployedBy("test_deployed_by")
.deployedDate(OffsetDateTime.now())
.deployedDate(getTimeAccurateToMicroSecond())
.podTerminationMessage(terminationMessage)
.containerTerminationReason(containerTerminationMessage)
.build();
Expand All @@ -256,8 +258,8 @@ private void testUpdateJobExecution(
actualDataJob,
actualExecutionSucceeded,
actualTerminationMessage,
OffsetDateTime.now(),
OffsetDateTime.now(),
getTimeAccurateToMicroSecond(),
getTimeAccurateToMicroSecond(),
"");
DataJobExecution actualJobExecution =
jobExecutionService.readJobExecution(
Expand Down
Loading