Skip to content

Commit

Permalink
Coordinate between reading shared objects and loading projects (gradl…
Browse files Browse the repository at this point in the history
  • Loading branch information
mlopatkin authored Nov 6, 2024
2 parents fcdde55 + 88e7ae8 commit ca3040e
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
api(projects.loggingApi)

api(libs.kotlinStdlib)
api(libs.inject)

implementation(projects.baseServices)
implementation(projects.serviceLookup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,26 @@

package org.gradle.internal.cc.base.serialize

import org.gradle.api.Project
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.build.BuildState
import org.gradle.internal.cc.base.services.ProjectRefResolver
import org.gradle.internal.serialize.graph.ReadContext
import org.gradle.internal.serialize.graph.WriteContext
import org.gradle.internal.serialize.graph.ownerService
import org.gradle.util.Path

/**
* Writes a reference to a project.
*/
fun WriteContext.writeProjectRef(project: Project) {
writeString(project.path)
}


fun ReadContext.getProject(path: String): ProjectInternal =
ownerService<BuildState>().projects.getProject(Path.path(path)).mutableModel
/**
* Reads a reference to a project. May block or throw if the projects haven't been loaded yet.
*
* @see ProjectRefResolver
*/
fun ReadContext.readProjectRef(): ProjectInternal {
return ownerService<ProjectRefResolver>().getProject(readString())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2024 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
*
* http://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 org.gradle.internal.cc.base.services

import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.build.BuildState
import org.gradle.internal.service.scopes.Scope
import org.gradle.internal.service.scopes.ServiceScope
import org.gradle.util.Path
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import kotlin.jvm.Throws

/**
* A stateful service that prevents races in `readProjectRef` when looking up projects by their paths.
* Threads may opt into waiting the until the project structure is restored when trying to resolve a project reference by calling [withWaitingForProjectsAllowed].
* Unless projects are loaded, an attempt to resolve a project without the opt-in throws an exception to avoid deadlocks.
*
* Using this class should be limited to serialization.
*
* This class is thread-safe.
*/
@ServiceScope(Scope.Build::class)
class ProjectRefResolver @Inject constructor(
private val buildState: BuildState
) {
private val projectReadyLock = CountDownLatch(1)

@Volatile
private var projectsLoaded = false

private val waitingForProjectsAllowed: MutableSet<Thread> = ConcurrentHashMap.newKeySet()

/**
* Allows this thread to wait for the project structure to be restored when calling [getProject] inside `block`.
*
* The thread that is responsible for loading the projects mustn't call this method to help detect potential deadlocks.
*/
fun withWaitingForProjectsAllowed(block: () -> Unit) {
val thread = Thread.currentThread()

val wasFirstToAdd = waitingForProjectsAllowed.add(thread)
try {
block()
} finally {
if (wasFirstToAdd) {
waitingForProjectsAllowed.remove(thread)
}
}
}

/**
* Marks projects as restored.
*/
fun projectsReady() {
projectsLoaded = true
projectReadyLock.countDown()
}

/**
* Resolves a project by a path relative to the root project or blocks until the projects are loaded if allowed.
*
* @throws InterruptedException if the thread is interrupted while waiting
* @throws TimeoutException if the projects weren't loaded within a reasonable time
* @throws IllegalStateException if the projects aren't ready and this thread isn't allowed to wait for them
*/
@Throws(InterruptedException::class, TimeoutException::class)
fun getProject(path: String): ProjectInternal {
waitForProjects(path)

return buildState.projects.getProject(Path.path(path)).mutableModel
}

private fun waitForProjects(projectPath: String) {
if (projectsLoaded) {
return
}

check(waitingForProjectsAllowed.contains(Thread.currentThread())) {
// This may happen if something that requires a project is deserialized before the project structure is ready.
"Cannot resolve project path '$projectPath' because project structure is not yet loaded for build '${buildState.identityPath}'"
}

if (!projectReadyLock.await(5, TimeUnit.MINUTES)) {
// We're typically wait here from a shared object decoder thread.
// It may take some time for the main decoding logic to get to the actual projects and restore them, so this timeout should be generous.
// TODO(mlopatkin) a deadlock is still possible when the main decoder waits for some shared object and the shared decoder is stuck waiting for projects.
// The blocked main thread is likely to give up earlier anyway.
throw TimeoutException("Cannot resolve project path '$projectPath' because project structure is not ready in time for build '${buildState.identityPath}'")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import org.gradle.internal.cc.base.serialize.IsolateOwners
import org.gradle.internal.cc.base.serialize.service
import org.gradle.internal.cc.base.serialize.withGradleIsolate
import org.gradle.internal.cc.base.services.ConfigurationCacheEnvironmentChangeTracker
import org.gradle.internal.cc.base.services.ProjectRefResolver
import org.gradle.internal.cc.impl.serialize.Codecs
import org.gradle.internal.configuration.problems.DocumentationSection.NotYetImplementedSourceDependencies
import org.gradle.internal.enterprise.core.GradleEnterprisePluginAdapter
Expand Down Expand Up @@ -487,6 +488,7 @@ class ConfigurationCacheState(
val projects = readProjects(gradle, build)

build.createProjects()
gradle.serviceOf<ProjectRefResolver>().projectsReady()

applyProjectStates(projects, gradle)
readRequiredBuildServicesOf(gradle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import org.gradle.internal.build.BuildModelControllerServices
import org.gradle.internal.build.BuildState
import org.gradle.internal.buildtree.BuildModelParameters
import org.gradle.internal.buildtree.IntermediateBuildActionRunner
import org.gradle.internal.cc.base.services.ProjectRefResolver
import org.gradle.internal.cc.impl.fingerprint.ConfigurationCacheFingerprintController
import org.gradle.internal.cc.impl.services.ConfigurationCacheEnvironment
import org.gradle.internal.cc.impl.services.DefaultEnvironment
Expand Down Expand Up @@ -78,6 +79,7 @@ class DefaultBuildModelControllerServices(
if (buildModelParameters.isConfigurationCache) {
registration.addProvider(ConfigurationCacheBuildControllerProvider())
registration.add(ConfigurationCacheEnvironment::class.java)
registration.add(ProjectRefResolver::class.java)
} else {
registration.addProvider(VintageBuildControllerProvider())
registration.add(DefaultEnvironment::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.gradle.internal.cc.impl.serialize

import org.gradle.internal.cc.base.services.ProjectRefResolver
import org.gradle.internal.extensions.stdlib.uncheckedCast
import org.gradle.internal.serialize.graph.CloseableReadContext
import org.gradle.internal.serialize.graph.CloseableWriteContext
Expand All @@ -24,6 +25,7 @@ import org.gradle.internal.serialize.graph.ReadContext
import org.gradle.internal.serialize.graph.SharedObjectDecoder
import org.gradle.internal.serialize.graph.SharedObjectEncoder
import org.gradle.internal.serialize.graph.WriteContext
import org.gradle.internal.serialize.graph.ownerService
import org.gradle.internal.serialize.graph.runReadOperation
import org.gradle.internal.serialize.graph.runWriteOperation
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -98,6 +100,7 @@ class DefaultSharedObjectDecoder(
val latch = CountDownLatch(1)

private
@Volatile
var value: Any? = null

fun complete(v: Any) {
Expand All @@ -107,11 +110,18 @@ class DefaultSharedObjectDecoder(

fun get(): Any {
val state = state.get()
if (value == null && state < ReaderState.STOPPED && !latch.await(1, TimeUnit.MINUTES)) {
// Only await if the reading thread is still running.
// This saves us a minute in case the reading code is broken and doesn't countDown() the latch properly.
// See the null check below.
if (state < ReaderState.STOPPED && !latch.await(1, TimeUnit.MINUTES)) {
throw TimeoutException("Timeout while waiting for value, state was $state")
}
require(value != null) { "State is: $state" }
return value!!
val result = value
require(result != null) {
// Reading thread hasn't written the value before completing/calling countDown(). This can only happen if the decoder has a bug.
"State is: $state"
}
return result
}
}

Expand All @@ -128,27 +138,32 @@ class DefaultSharedObjectDecoder(
"Unexpected state: $state"
}
globalContext.run {
while (state.get() == ReaderState.RUNNING) {
val id = readSmallInt()
if (id == EOF) {
stopReading()
break
}
val read = runReadOperation {
read()!!
}
values.compute(id) { _, value ->
when (value) {
is FutureValue -> value.complete(read)
else -> require(value == null)
projectRefResolver.withWaitingForProjectsAllowed {
while (state.get() == ReaderState.RUNNING) {
val id = readSmallInt()
if (id == EOF) {
stopReading()
break
}
val read = runReadOperation {
read()!!
}
values.compute(id) { _, value ->
when (value) {
is FutureValue -> value.complete(read)
else -> require(value == null)
}
read
}
read
}
}
state.set(ReaderState.STOPPED)
}
}

private val ReadContext.projectRefResolver
get() = ownerService<ProjectRefResolver>()

private
fun ReadContext.resolveValue(id: Int): Any {
startReadingIfNeeded()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ import org.gradle.api.internal.GeneratedSubclasses
import org.gradle.api.internal.TaskInputsInternal
import org.gradle.api.internal.TaskInternal
import org.gradle.api.internal.TaskOutputsInternal
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.internal.provider.Providers
import org.gradle.api.internal.tasks.TaskDestroyablesInternal
import org.gradle.api.internal.tasks.TaskInputFilePropertyBuilderInternal
import org.gradle.api.internal.tasks.TaskLocalStateInternal
import org.gradle.api.specs.Spec
import org.gradle.internal.cc.base.serialize.IsolateOwners
import org.gradle.execution.plan.LocalTaskNode
import org.gradle.execution.plan.TaskNodeFactory
import org.gradle.internal.cc.base.serialize.IsolateOwners
import org.gradle.internal.cc.base.serialize.readProjectRef
import org.gradle.internal.cc.base.serialize.writeProjectRef
import org.gradle.internal.configuration.problems.PropertyKind
import org.gradle.internal.configuration.problems.PropertyTrace
import org.gradle.internal.cc.base.serialize.getProject
import org.gradle.internal.execution.model.InputNormalizer
import org.gradle.internal.extensions.stdlib.uncheckedCast
import org.gradle.internal.fingerprint.DirectorySensitivity
Expand Down Expand Up @@ -84,10 +86,9 @@ class TaskNodeCodec(
suspend fun WriteContext.writeTask(task: TaskInternal) {
withDebugFrame({ task.path }) {
val taskType = GeneratedSubclasses.unpackType(task)
val projectPath = task.project.path
val taskName = task.name
writeClass(taskType)
writeString(projectPath)
writeProjectRef(task.project)
writeString(taskName)
writeLong(task.taskIdentity.uniqueId)
writeNullableString(task.reasonTaskIsIncompatibleWithConfigurationCache.orElse(null))
Expand Down Expand Up @@ -117,12 +118,12 @@ class TaskNodeCodec(
private
suspend fun ReadContext.readTask(): Task {
val taskType = readClassOf<Task>()
val projectPath = readString()
val project = readProjectRef()
val taskName = readString()
val uniqueId = readLong()
val incompatibleReason = readNullableString()

val task = createTask(projectPath, taskName, taskType, uniqueId, incompatibleReason)
val task = createTask(project, taskName, taskType, uniqueId, incompatibleReason)

withTaskOf(taskType, task, userTypesCodec) {
readUpToDateSpec(task)
Expand Down Expand Up @@ -474,8 +475,8 @@ suspend fun ReadContext.readOutputPropertiesOf(task: Task) =


private
fun ReadContext.createTask(projectPath: String, taskName: String, taskClass: Class<out Task>, uniqueId: Long, incompatibleReason: String?): TaskInternal {
val task = getProject(projectPath).tasks.createWithoutConstructor(taskName, taskClass, uniqueId) as TaskInternal
fun createTask(project: ProjectInternal, taskName: String, taskClass: Class<out Task>, uniqueId: Long, incompatibleReason: String?): TaskInternal {
val task = project.tasks.createWithoutConstructor(taskName, taskClass, uniqueId) as TaskInternal
if (incompatibleReason != null) {
task.notCompatibleWithConfigurationCache(incompatibleReason)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import org.gradle.api.internal.DomainObjectContext
import org.gradle.api.internal.artifacts.transform.Transform
import org.gradle.api.internal.artifacts.transform.TransformInvocationFactory
import org.gradle.api.internal.artifacts.transform.TransformStep
import org.gradle.internal.cc.base.serialize.getProject
import org.gradle.internal.cc.base.serialize.readProjectRef
import org.gradle.internal.cc.base.serialize.writeProjectRef
import org.gradle.internal.execution.InputFingerprinter
import org.gradle.internal.serialize.graph.Codec
import org.gradle.internal.serialize.graph.ReadContext
Expand All @@ -37,16 +38,15 @@ class TransformStepCodec(
override suspend fun WriteContext.encode(value: TransformStep) {
encodePreservingSharedIdentityOf(value) {
val project = value.owningProject ?: throw UnsupportedOperationException("TransformStep must have an owning project to be encoded.")
writeString(project.path)
writeProjectRef(project)
write(value.transform)
}
}

override suspend fun ReadContext.decode(): TransformStep {
return decodePreservingSharedIdentity {
val path = readString()
val project = readProjectRef()
val transform = readNonNull<Transform>()
val project = getProject(path)
val services = project.services
TransformStep(
transform,
Expand Down

0 comments on commit ca3040e

Please sign in to comment.